From da6c6dcfcbbce9f1f5e1addbf102301f5f51abf3 Mon Sep 17 00:00:00 2001 From: NaviN <118178330+Navin-S-R@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:23:16 +0530 Subject: [PATCH 1/2] fix: update per_billed value in Purchase Receipt while creating Debit Note (#43977) * fix: update per_billed value in Purchase Receipt while creating Debit Note * test: add unit test for validating per_billed value for partial Debit Note (cherry picked from commit 494fd7ceeab3edd56f7f4975101a611e9e8ad391) # Conflicts: # erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py --- .../purchase_receipt/purchase_receipt.py | 11 +- .../purchase_receipt/test_purchase_receipt.py | 279 ++++++++++++++++++ 2 files changed, 288 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 8ed59f452e22..faf305cfe9ce 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -883,6 +883,8 @@ def get_billed_amount_against_po(po_items): def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): # Update Billing % based on pending accepted qty + buying_settings = frappe.get_single("Buying Settings") + total_amount, total_billed_amount = 0, 0 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) @@ -890,10 +892,15 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate returned_qty = flt(item_wise_returned_qty.get(item.name)) returned_amount = flt(returned_qty) * flt(item.rate) pending_amount = flt(item.amount) - returned_amount - total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt + if buying_settings.bill_for_rejected_quantity_in_purchase_invoice: + pending_amount = flt(item.amount) + + total_billable_amount = abs(flt(item.amount)) + if pending_amount > 0: + total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt total_amount += total_billable_amount - total_billed_amount += flt(item.billed_amt) + total_billed_amount += abs(flt(item.billed_amt)) if pr_doc.get("is_return") and not total_amount and total_billed_amount: total_amount = total_billed_amount diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2525120cefd8..890502c797f4 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2683,6 +2683,285 @@ def test_same_stock_and_transaction_uom_conversion_factor(self): self.assertEqual(pr.items[0].conversion_factor, 1.0) +<<<<<<< HEAD +======= + def test_purchase_receipt_return_valuation_without_use_serial_batch_field(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return + + batch_item = make_item( + "_Test Purchase Receipt Return Valuation Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-TPRBI-.#####", + }, + ).name + + serial_item = make_item( + "_Test Purchase Receipt Return Valuation Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-TPRSI-.#####"}, + ).name + + rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty") + + pr = make_purchase_receipt( + item_code=batch_item, + received_qty=10, + qty=8, + rejected_qty=2, + rejected_warehouse=rej_warehouse, + rate=300, + do_not_submit=1, + use_serial_batch_fields=0, + ) + + pr.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 100, + "base_rate": 100, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "rejected_qty": 1, + "warehouse": pr.items[0].warehouse, + "use_serial_batch_fields": 0, + "rejected_warehouse": rej_warehouse, + }, + ) + + pr.save() + pr.submit() + pr.reload() + + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle) + + self.assertEqual(batch_no, rejected_batch_no) + + return_entry = make_purchase_return(pr.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 300.00) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 100.00) + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + + def test_purchase_receipt_return_valuation_with_use_serial_batch_field(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return + + batch_item = make_item( + "_Test Purchase Receipt Return Valuation With Batch Item", + properties={"has_batch_no": 1, "create_new_batch": 1, "is_stock_item": 1}, + ).name + + serial_item = make_item( + "_Test Purchase Receipt Return Valuation With Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1}, + ).name + + rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty") + + batch_no = "BATCH-RTN-BNU-TPRBI-0001" + serial_nos = ["SNU-RTN-TPRSI-0001", "SNU-RTN-TPRSI-0002", "SNU-RTN-TPRSI-0003"] + + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_no, + "item": batch_item, + } + ).insert() + + for serial_no in serial_nos: + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + { + "doctype": "Serial No", + "item_code": serial_item, + "serial_no": serial_no, + } + ).insert() + + pr = make_purchase_receipt( + item_code=batch_item, + received_qty=10, + qty=8, + rejected_qty=2, + rejected_warehouse=rej_warehouse, + batch_no=batch_no, + use_serial_batch_fields=1, + rate=300, + do_not_submit=1, + ) + + pr.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 100, + "base_rate": 100, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "rejected_qty": 1, + "warehouse": pr.items[0].warehouse, + "use_serial_batch_fields": 1, + "rejected_warehouse": rej_warehouse, + "serial_no": "\n".join(serial_nos[:2]), + "rejected_serial_no": serial_nos[2], + }, + ) + + pr.save() + pr.submit() + pr.reload() + + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle) + + self.assertEqual(batch_no, rejected_batch_no) + + return_entry = make_purchase_return(pr.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 300.00) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 100.00) + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + + def test_purchase_return_partial_debit_note(self): + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + ) + + return_pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + is_return=1, + return_against=pr.name, + qty=-2, + do_not_submit=1, + ) + return_pr.items[0].purchase_receipt_item = pr.items[0].name + return_pr.submit() + + # because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Purchase Receipt", return_pr.name) + returned.update_prevdoc_status() + pr.load_from_db() + + # Check if Original PR updated + self.assertEqual(pr.items[0].returned_qty, 2) + self.assertEqual(pr.per_returned, 40) + + # Create first partial debit_note + pi_1 = make_purchase_invoice(return_pr.name) + pi_1.items[0].qty = -1 + pi_1.submit() + + # Check if the first partial debit billing percentage got updated + return_pr.reload() + self.assertEqual(return_pr.per_billed, 50) + self.assertEqual(return_pr.status, "Partly Billed") + + # Create second partial debit_note to complete the debit note + pi_2 = make_purchase_invoice(return_pr.name) + pi_2.items[0].qty = -1 + pi_2.submit() + + # Check if the second partial debit note billing percentage got updated + return_pr.reload() + self.assertEqual(return_pr.per_billed, 100) + self.assertEqual(return_pr.status, "Completed") + +>>>>>>> 494fd7ceea (fix: update per_billed value in Purchase Receipt while creating Debit Note (#43977)) def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 030df0d7e889c40c9af73cbc1febd981c978dbf4 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 13 Nov 2024 12:25:24 +0530 Subject: [PATCH 2/2] chore: fix conflicts --- .../purchase_receipt/test_purchase_receipt.py | 231 ------------------ 1 file changed, 231 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 890502c797f4..adb6e6905964 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2683,236 +2683,6 @@ def test_same_stock_and_transaction_uom_conversion_factor(self): self.assertEqual(pr.items[0].conversion_factor, 1.0) -<<<<<<< HEAD -======= - def test_purchase_receipt_return_valuation_without_use_serial_batch_field(self): - from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return - - batch_item = make_item( - "_Test Purchase Receipt Return Valuation Batch Item", - properties={ - "has_batch_no": 1, - "create_new_batch": 1, - "is_stock_item": 1, - "batch_number_series": "BRTN-TPRBI-.#####", - }, - ).name - - serial_item = make_item( - "_Test Purchase Receipt Return Valuation Serial Item", - properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-TPRSI-.#####"}, - ).name - - rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty") - - pr = make_purchase_receipt( - item_code=batch_item, - received_qty=10, - qty=8, - rejected_qty=2, - rejected_warehouse=rej_warehouse, - rate=300, - do_not_submit=1, - use_serial_batch_fields=0, - ) - - pr.append( - "items", - { - "item_code": serial_item, - "qty": 2, - "rate": 100, - "base_rate": 100, - "item_name": serial_item, - "uom": "Nos", - "stock_uom": "Nos", - "conversion_factor": 1, - "rejected_qty": 1, - "warehouse": pr.items[0].warehouse, - "use_serial_batch_fields": 0, - "rejected_warehouse": rej_warehouse, - }, - ) - - pr.save() - pr.submit() - pr.reload() - - batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) - rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle) - - self.assertEqual(batch_no, rejected_batch_no) - - return_entry = make_purchase_return(pr.name) - - return_entry.save() - return_entry.submit() - return_entry.reload() - - for row in return_entry.items: - if row.item_code == batch_item: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - pluck="incoming_rate", - ) - - for incoming_rate in bundle_data: - self.assertEqual(incoming_rate, 300.00) - else: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - pluck="incoming_rate", - ) - - for incoming_rate in bundle_data: - self.assertEqual(incoming_rate, 100.00) - - for row in return_entry.items: - if row.item_code == batch_item: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.rejected_serial_and_batch_bundle}, - pluck="incoming_rate", - ) - - for incoming_rate in bundle_data: - self.assertEqual(incoming_rate, 0) - else: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.rejected_serial_and_batch_bundle}, - pluck="incoming_rate", - ) - - for incoming_rate in bundle_data: - self.assertEqual(incoming_rate, 0) - - def test_purchase_receipt_return_valuation_with_use_serial_batch_field(self): - from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return - - batch_item = make_item( - "_Test Purchase Receipt Return Valuation With Batch Item", - properties={"has_batch_no": 1, "create_new_batch": 1, "is_stock_item": 1}, - ).name - - serial_item = make_item( - "_Test Purchase Receipt Return Valuation With Serial Item", - properties={"has_serial_no": 1, "is_stock_item": 1}, - ).name - - rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty") - - batch_no = "BATCH-RTN-BNU-TPRBI-0001" - serial_nos = ["SNU-RTN-TPRSI-0001", "SNU-RTN-TPRSI-0002", "SNU-RTN-TPRSI-0003"] - - if not frappe.db.exists("Batch", batch_no): - frappe.get_doc( - { - "doctype": "Batch", - "batch_id": batch_no, - "item": batch_item, - } - ).insert() - - for serial_no in serial_nos: - if not frappe.db.exists("Serial No", serial_no): - frappe.get_doc( - { - "doctype": "Serial No", - "item_code": serial_item, - "serial_no": serial_no, - } - ).insert() - - pr = make_purchase_receipt( - item_code=batch_item, - received_qty=10, - qty=8, - rejected_qty=2, - rejected_warehouse=rej_warehouse, - batch_no=batch_no, - use_serial_batch_fields=1, - rate=300, - do_not_submit=1, - ) - - pr.append( - "items", - { - "item_code": serial_item, - "qty": 2, - "rate": 100, - "base_rate": 100, - "item_name": serial_item, - "uom": "Nos", - "stock_uom": "Nos", - "conversion_factor": 1, - "rejected_qty": 1, - "warehouse": pr.items[0].warehouse, - "use_serial_batch_fields": 1, - "rejected_warehouse": rej_warehouse, - "serial_no": "\n".join(serial_nos[:2]), - "rejected_serial_no": serial_nos[2], - }, - ) - - pr.save() - pr.submit() - pr.reload() - - batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) - rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle) - - self.assertEqual(batch_no, rejected_batch_no) - - return_entry = make_purchase_return(pr.name) - - return_entry.save() - return_entry.submit() - return_entry.reload() - - for row in return_entry.items: - if row.item_code == batch_item: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - pluck="incoming_rate", - ) - - for incoming_rate in bundle_data: - self.assertEqual(incoming_rate, 300.00) - else: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - pluck="incoming_rate", - ) - - for incoming_rate in bundle_data: - self.assertEqual(incoming_rate, 100.00) - - for row in return_entry.items: - if row.item_code == batch_item: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.rejected_serial_and_batch_bundle}, - pluck="incoming_rate", - ) - - for incoming_rate in bundle_data: - self.assertEqual(incoming_rate, 0) - else: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.rejected_serial_and_batch_bundle}, - pluck="incoming_rate", - ) - - for incoming_rate in bundle_data: - self.assertEqual(incoming_rate, 0) - def test_purchase_return_partial_debit_note(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", @@ -2961,7 +2731,6 @@ def test_purchase_return_partial_debit_note(self): self.assertEqual(return_pr.per_billed, 100) self.assertEqual(return_pr.status, "Completed") ->>>>>>> 494fd7ceea (fix: update per_billed value in Purchase Receipt while creating Debit Note (#43977)) def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier