From 4e215c6b7b88562dcc1bac5265180dd048dc07a3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 11 Apr 2024 16:20:55 +0530 Subject: [PATCH 01/22] fix: landed cost voucher for legacy pr with batch (cherry picked from commit fa91cda46cd1271cbd442a4d988cf6775cc64788) --- erpnext/controllers/stock_controller.py | 4 +- .../landed_cost_voucher.py | 1 + .../test_landed_cost_voucher.py | 149 ++++++++++++++++++ 3 files changed, 152 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index af0afb4dd02..73436508282 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -166,7 +166,7 @@ class StockController(AccountsController): # remove extra whitespace and store one serial no on each line row.serial_no = clean_serial_no_string(row.serial_no) - def make_bundle_using_old_serial_batch_fields(self, table_name=None): + def make_bundle_using_old_serial_batch_fields(self, table_name=None, via_landed_cost_voucher=False): if self.get("_action") == "update_after_submit": return @@ -205,7 +205,7 @@ class StockController(AccountsController): "company": self.company, "is_rejected": 1 if row.get("rejected_warehouse") else 0, "use_serial_batch_fields": row.use_serial_batch_fields, - "do_not_submit": True, + "do_not_submit": True if not via_landed_cost_voucher else False, } if row.get("qty") or row.get("consumed_qty"): diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index ff6d31a777f..601cde6c905 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -250,6 +250,7 @@ class LandedCostVoucher(Document): # update stock & gl entries for submit state of PR doc.docstatus = 1 + doc.make_bundle_using_old_serial_batch_fields(via_landed_cost_voucher=True) doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) doc.make_gl_entries() doc.repost_future_sle_and_gle() diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 9ec2d695707..32b384def28 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -596,6 +596,155 @@ class TestLandedCostVoucher(FrappeTestCase): lcv.cancel() pr.cancel() + def test_landed_cost_voucher_with_serial_batch_for_legacy_pr(self): + from erpnext.stock.doctype.item.test_item import make_item + + frappe.flags.ignore_serial_batch_bundle_validation = True + frappe.flags.use_serial_and_batch_fields = True + sn_item = "Test Landed Cost Voucher Serial NO for Legacy PR" + batch_item = "Test Landed Cost Voucher Batch NO for Legacy PR" + sn_item_doc = make_item( + sn_item, + { + "has_serial_no": 1, + "serial_no_series": "SN-TLCVSNO-.####", + "is_stock_item": 1, + }, + ) + + batch_item_doc = make_item( + batch_item, + { + "has_batch_no": 1, + "batch_number_series": "BATCH-TLCVSNO-.####", + "create_new_batch": 1, + "is_stock_item": 1, + }, + ) + + serial_nos = [ + "SN-TLCVSNO-0001", + "SN-TLCVSNO-0002", + "SN-TLCVSNO-0003", + "SN-TLCVSNO-0004", + "SN-TLCVSNO-0005", + ] + + for sn in serial_nos: + if not frappe.db.exists("Serial No", sn): + sn_doc = frappe.get_doc( + { + "doctype": "Serial No", + "item_code": sn_item, + "serial_no": sn, + } + ) + sn_doc.insert() + + if not frappe.db.exists("Batch", "BATCH-TLCVSNO-0001"): + batch_doc = frappe.get_doc( + { + "doctype": "Batch", + "item": batch_item, + "batch_id": "BATCH-TLCVSNO-0001", + } + ) + batch_doc.insert() + + warehouse = "_Test Warehouse - _TC" + company = frappe.db.get_value("Warehouse", warehouse, "company") + + pr = make_purchase_receipt( + company=company, + warehouse=warehouse, + item_code=sn_item, + qty=5, + rate=100, + uom=sn_item_doc.stock_uom, + stock_uom=sn_item_doc.stock_uom, + do_not_submit=True, + ) + + pr.append( + "items", + { + "item_code": batch_item, + "item_name": batch_item, + "description": "Test Batch Item", + "uom": batch_item_doc.stock_uom, + "stock_uom": batch_item_doc.stock_uom, + "qty": 5, + "rate": 100, + "warehouse": warehouse, + }, + ) + + pr.submit() + pr.reload() + + for row in pr.items: + self.assertEqual(row.valuation_rate, 100) + self.assertFalse(row.serial_no) + self.assertFalse(row.batch_no) + self.assertFalse(row.serial_and_batch_bundle) + + if row.item_code == sn_item: + row.db_set("serial_no", ", ".join(serial_nos)) + else: + row.db_set("batch_no", "BATCH-TLCVSNO-0001") + + for sn in serial_nos: + sn_doc = frappe.get_doc("Serial No", sn) + sn_doc.db_set( + { + "warehouse": warehouse, + "status": "Active", + } + ) + + batch_doc.db_set( + { + "batch_qty": 5, + } + ) + + frappe.flags.ignore_serial_batch_bundle_validation = False + frappe.flags.use_serial_and_batch_fields = False + + lcv = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=20, + distribute_charges_based_on="Qty", + do_not_save=True, + ) + + lcv.get_items_from_purchase_receipts() + lcv.save() + lcv.submit() + + pr.reload() + + for row in pr.items: + self.assertEqual(row.valuation_rate, 102) + self.assertTrue(row.serial_and_batch_bundle) + self.assertEqual( + row.valuation_rate, + frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"), + ) + + lcv.cancel() + pr.reload() + + for row in pr.items: + self.assertEqual(row.valuation_rate, 100) + self.assertTrue(row.serial_and_batch_bundle) + self.assertEqual( + row.valuation_rate, + frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"), + ) + def make_landed_cost_voucher(**args): args = frappe._dict(args) From e9c6c5a8eb47daa063fbbbeb74284fda338c1b80 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 11 Apr 2024 17:35:06 +0530 Subject: [PATCH 02/22] fix: zero division error (cherry picked from commit f9e230e758f16a6715534648dadc1003af98401a) --- erpnext/controllers/selling_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index ef7a5b69f92..e9896fa0fd6 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -444,7 +444,7 @@ class SellingController(StockController): get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") ): # Get incoming rate based on original item cost based on valuation method - qty = flt(d.get("stock_qty") or d.get("actual_qty")) + qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty")) if ( not d.incoming_rate From 0053d57ec4ef33c022d86951fc484d1d87fe5bee Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 12 Apr 2024 09:26:46 +0530 Subject: [PATCH 03/22] fix: Link Validation Error on Dunning cancellation (cherry picked from commit 205fd9888cb943340a4bddb0ea10258fc882e75f) --- erpnext/accounts/doctype/dunning/dunning.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index c61c3329c69..2719c83ba35 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -139,6 +139,10 @@ class Dunning(AccountsController): ) row.dunning_level = len(past_dunnings) + 1 + def on_cancel(self): + super().on_cancel() + self.ignore_linked_doctypes = ["GL Entry"] + def resolve_dunning(doc, state): """ From 14a1a18243c54108e5e29d1878b5bc7ea97387b6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:28:17 +0530 Subject: [PATCH 04/22] fix(ux): Sales Order Stock Reservation Dialog (backport #40707) (#40980) fix(ux): Sales Order Stock Reservation Dialog (cherry picked from commit 5daf19da409324e639a799e3bf961ca4f61091a5) Co-authored-by: s-aga-r --- .../doctype/sales_order/sales_order.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 8d08a37aff2..2ee7543b3c0 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -288,6 +288,7 @@ frappe.ui.form.on("Sales Order", { label: __("Items to Reserve"), allow_bulk_edit: false, cannot_add_rows: true, + cannot_delete_rows: true, data: [], fields: [ { @@ -356,7 +357,7 @@ frappe.ui.form.on("Sales Order", { ], primary_action_label: __("Reserve Stock"), primary_action: () => { - var data = { items: dialog.fields_dict.items.grid.data }; + var data = { items: dialog.fields_dict.items.grid.get_selected_children() }; if (data.items && data.items.length > 0) { frappe.call({ @@ -373,9 +374,11 @@ frappe.ui.form.on("Sales Order", { frm.reload_doc(); }, }); - } - dialog.hide(); + dialog.hide(); + } else { + frappe.msgprint(__("Please select items to reserve.")); + } }, }); @@ -390,6 +393,7 @@ frappe.ui.form.on("Sales Order", { if (unreserved_qty > 0) { dialog.fields_dict.items.df.data.push({ + __checked: 1, sales_order_item: item.name, item_code: item.item_code, warehouse: item.warehouse, @@ -414,6 +418,7 @@ frappe.ui.form.on("Sales Order", { label: __("Reserved Stock"), allow_bulk_edit: false, cannot_add_rows: true, + cannot_delete_rows: true, in_place_edit: true, data: [], fields: [ @@ -457,7 +462,7 @@ frappe.ui.form.on("Sales Order", { ], primary_action_label: __("Unreserve Stock"), primary_action: () => { - var data = { sr_entries: dialog.fields_dict.sr_entries.grid.data }; + var data = { sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children() }; if (data.sr_entries && data.sr_entries.length > 0) { frappe.call({ @@ -473,9 +478,11 @@ frappe.ui.form.on("Sales Order", { frm.reload_doc(); }, }); - } - dialog.hide(); + dialog.hide(); + } else { + frappe.msgprint(__("Please select items to unreserve.")); + } }, }); From 5de9b6ac757246c6578af32f521398ca1629d51d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 12 Apr 2024 11:25:34 +0530 Subject: [PATCH 05/22] fix: do not validate batch qty for LCV (cherry picked from commit baf0c83cc5c98bf74a6a437f4bec2131074a9ae0) --- erpnext/controllers/stock_controller.py | 15 +- .../landed_cost_voucher.py | 2 +- .../test_landed_cost_voucher.py | 201 ++++++++++++++++++ .../serial_and_batch_bundle.py | 6 + 4 files changed, 220 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 73436508282..219a1d68c52 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1119,7 +1119,7 @@ class StockController(AccountsController): message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link) return message - def repost_future_sle_and_gle(self, force=False): + def repost_future_sle_and_gle(self, force=False, via_landed_cost_voucher=False): args = frappe._dict( { "posting_date": self.posting_date, @@ -1127,6 +1127,7 @@ class StockController(AccountsController): "voucher_type": self.doctype, "voucher_no": self.name, "company": self.company, + "via_landed_cost_voucher": via_landed_cost_voucher, } ) @@ -1138,7 +1139,11 @@ class StockController(AccountsController): 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) + create_item_wise_repost_entries( + voucher_type=self.doctype, + voucher_no=self.name, + via_landed_cost_voucher=via_landed_cost_voucher, + ) else: create_repost_item_valuation_entry(args) @@ -1510,11 +1515,14 @@ def create_repost_item_valuation_entry(args): repost_entry.allow_zero_rate = args.allow_zero_rate repost_entry.flags.ignore_links = True repost_entry.flags.ignore_permissions = True + repost_entry.via_landed_cost_voucher = args.via_landed_cost_voucher repost_entry.save() repost_entry.submit() -def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=False): +def create_item_wise_repost_entries( + voucher_type, voucher_no, allow_zero_rate=False, via_landed_cost_voucher=False +): """Using a voucher create repost item valuation records for all item-warehouse pairs.""" stock_ledger_entries = get_items_to_be_repost(voucher_type, voucher_no) @@ -1538,6 +1546,7 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa repost_entry.allow_zero_rate = allow_zero_rate repost_entry.flags.ignore_links = True repost_entry.flags.ignore_permissions = True + repost_entry.via_landed_cost_voucher = via_landed_cost_voucher repost_entry.submit() repost_entries.append(repost_entry) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 601cde6c905..683e946298a 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -253,7 +253,7 @@ class LandedCostVoucher(Document): doc.make_bundle_using_old_serial_batch_fields(via_landed_cost_voucher=True) doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) doc.make_gl_entries() - doc.repost_future_sle_and_gle() + doc.repost_future_sle_and_gle(via_landed_cost_voucher=True) def validate_asset_qty_and_status(self, receipt_document_type, receipt_document): for item in self.get("items"): diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 32b384def28..13b7f97b7c4 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -745,6 +745,207 @@ class TestLandedCostVoucher(FrappeTestCase): frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"), ) + def test_do_not_validate_landed_cost_voucher_with_serial_batch_for_legacy_pr(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import get_auto_batch_nos + + frappe.flags.ignore_serial_batch_bundle_validation = True + frappe.flags.use_serial_and_batch_fields = True + sn_item = "Test Don't Validate Landed Cost Voucher Serial NO for Legacy PR" + batch_item = "Test Don't Validate Landed Cost Voucher Batch NO for Legacy PR" + sn_item_doc = make_item( + sn_item, + { + "has_serial_no": 1, + "serial_no_series": "SN-TDVLCVSNO-.####", + "is_stock_item": 1, + }, + ) + + batch_item_doc = make_item( + batch_item, + { + "has_batch_no": 1, + "batch_number_series": "BATCH-TDVLCVSNO-.####", + "create_new_batch": 1, + "is_stock_item": 1, + }, + ) + + serial_nos = [ + "SN-TDVLCVSNO-0001", + "SN-TDVLCVSNO-0002", + "SN-TDVLCVSNO-0003", + "SN-TDVLCVSNO-0004", + "SN-TDVLCVSNO-0005", + ] + + for sn in serial_nos: + if not frappe.db.exists("Serial No", sn): + sn_doc = frappe.get_doc( + { + "doctype": "Serial No", + "item_code": sn_item, + "serial_no": sn, + } + ) + sn_doc.insert() + + if not frappe.db.exists("Batch", "BATCH-TDVLCVSNO-0001"): + batch_doc = frappe.get_doc( + { + "doctype": "Batch", + "item": batch_item, + "batch_id": "BATCH-TDVLCVSNO-0001", + } + ) + batch_doc.insert() + + warehouse = "_Test Warehouse - _TC" + company = frappe.db.get_value("Warehouse", warehouse, "company") + + pr = make_purchase_receipt( + company=company, + warehouse=warehouse, + item_code=sn_item, + qty=5, + rate=100, + uom=sn_item_doc.stock_uom, + stock_uom=sn_item_doc.stock_uom, + do_not_submit=True, + ) + + pr.append( + "items", + { + "item_code": batch_item, + "item_name": batch_item, + "description": "Test Batch Item", + "uom": batch_item_doc.stock_uom, + "stock_uom": batch_item_doc.stock_uom, + "qty": 5, + "rate": 100, + "warehouse": warehouse, + }, + ) + + pr.submit() + pr.reload() + + for sn in serial_nos: + sn_doc = frappe.get_doc("Serial No", sn) + sn_doc.db_set( + { + "warehouse": warehouse, + "status": "Active", + } + ) + + batch_doc.db_set( + { + "batch_qty": 5, + } + ) + + for row in pr.items: + if row.item_code == sn_item: + row.db_set("serial_no", ", ".join(serial_nos)) + else: + row.db_set("batch_no", "BATCH-TDVLCVSNO-0001") + + stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": pr.name}) + for sle in stock_ledger_entries: + doc = frappe.get_doc("Stock Ledger Entry", sle.name) + if doc.item_code == sn_item: + doc.db_set("serial_no", ", ".join(serial_nos)) + else: + doc.db_set("batch_no", "BATCH-TDVLCVSNO-0001") + + dn = create_delivery_note( + company=company, + warehouse=warehouse, + item_code=sn_item, + qty=5, + rate=100, + uom=sn_item_doc.stock_uom, + stock_uom=sn_item_doc.stock_uom, + do_not_submit=True, + ) + + dn.append( + "items", + { + "item_code": batch_item, + "item_name": batch_item, + "description": "Test Batch Item", + "uom": batch_item_doc.stock_uom, + "stock_uom": batch_item_doc.stock_uom, + "qty": 5, + "rate": 100, + "warehouse": warehouse, + }, + ) + + dn.submit() + + stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": dn.name}) + for sle in stock_ledger_entries: + doc = frappe.get_doc("Stock Ledger Entry", sle.name) + if doc.item_code == sn_item: + doc.db_set("serial_no", ", ".join(serial_nos)) + else: + doc.db_set("batch_no", "BATCH-TDVLCVSNO-0001") + + available_batches = get_auto_batch_nos( + frappe._dict( + { + "item_code": batch_item, + "warehouse": warehouse, + "batch_no": ["BATCH-TDVLCVSNO-0001"], + "consider_negative_batches": True, + } + ) + )[0] + + self.assertFalse(available_batches.get("qty")) + + frappe.flags.ignore_serial_batch_bundle_validation = False + frappe.flags.use_serial_and_batch_fields = False + + lcv = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=20, + distribute_charges_based_on="Qty", + do_not_save=True, + ) + + lcv.get_items_from_purchase_receipts() + lcv.save() + lcv.submit() + + pr.reload() + + for row in pr.items: + self.assertEqual(row.valuation_rate, 102) + self.assertTrue(row.serial_and_batch_bundle) + self.assertEqual( + row.valuation_rate, + frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"), + ) + + lcv.cancel() + pr.reload() + + for row in pr.items: + self.assertEqual(row.valuation_rate, 100) + self.assertTrue(row.serial_and_batch_bundle) + self.assertEqual( + row.valuation_rate, + frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"), + ) + def make_landed_cost_voucher(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 19aad3f6296..286a220c5dd 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -860,6 +860,12 @@ class SerialandBatchBundle(Document): self.validate_batch_inventory() def validate_batch_inventory(self): + if ( + self.voucher_type in ["Purchase Invoice", "Purchase Receipt"] + and frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1 + ): + return + if not self.has_batch_no: return From 9abc71f9c8f0335c986d4f3953a6147ec77bb557 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:15:11 +0530 Subject: [PATCH 06/22] fix: Subcontracting Receipt GL Entries (backport #40773) (#40979) * fix: Subcontracting Receipt GL Entries (cherry picked from commit 9808ae92a44328f357c224e2f61c89e79c9255f7) # Conflicts: # erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../subcontracting_receipt.py | 95 ++++++++++++------- .../test_subcontracting_receipt.py | 69 ++++++++++---- 2 files changed, 116 insertions(+), 48 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 87595289179..cc800e4369d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -498,9 +498,6 @@ class SubcontractingReceipt(SubcontractingController): return process_gl_map(gl_entries) def make_item_gl_entries(self, gl_entries, warehouse_account=None): - stock_rbnb = self.get_company_default("stock_received_but_not_billed") - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") - warehouse_with_no_account = [] for item in self.items: @@ -518,31 +515,41 @@ class SubcontractingReceipt(SubcontractingController): "stock_value_difference", ) - warehouse_account_name = warehouse_account[item.warehouse]["account"] - warehouse_account_currency = warehouse_account[item.warehouse]["account_currency"] + accepted_warehouse_account = warehouse_account[item.warehouse]["account"] supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get( "account" ) - supplier_warehouse_account_currency = warehouse_account.get( - self.supplier_warehouse, {} - ).get("account_currency") remarks = self.get("remarks") or _("Accounting Entry for Stock") - # FG Warehouse Account (Debit) + # Accepted Warehouse Account (Debit) self.add_gl_entry( gl_entries=gl_entries, - account=warehouse_account_name, + account=accepted_warehouse_account, cost_center=item.cost_center, debit=stock_value_diff, credit=0.0, remarks=remarks, - against_account=stock_rbnb, - account_currency=warehouse_account_currency, + against_account=item.expense_account, + account_currency=get_account_currency(accepted_warehouse_account), + project=item.project, + item=item, + ) + # Expense Account (Credit) + self.add_gl_entry( + gl_entries=gl_entries, + account=item.expense_account, + cost_center=item.cost_center, + debit=0.0, + credit=stock_value_diff, + remarks=remarks, + against_account=accepted_warehouse_account, + account_currency=get_account_currency(item.expense_account), + project=item.project, item=item, ) - # Supplier Warehouse Account (Credit) - if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): + if flt(item.rm_supp_cost) and supplier_warehouse_account: + # Supplier Warehouse Account (Credit) self.add_gl_entry( gl_entries=gl_entries, account=supplier_warehouse_account, @@ -550,43 +557,66 @@ class SubcontractingReceipt(SubcontractingController): debit=0.0, credit=flt(item.rm_supp_cost), remarks=remarks, - against_account=warehouse_account_name, - account_currency=supplier_warehouse_account_currency, + against_account=item.expense_account, + account_currency=get_account_currency(supplier_warehouse_account), + project=item.project, item=item, ) - - # Expense Account (Credit) - if flt(item.service_cost_per_qty): + # Expense Account (Debit) self.add_gl_entry( gl_entries=gl_entries, account=item.expense_account, cost_center=item.cost_center, - debit=0.0, - credit=flt(item.service_cost_per_qty) * flt(item.qty), + debit=flt(item.rm_supp_cost), + credit=0.0, remarks=remarks, - against_account=warehouse_account_name, + against_account=supplier_warehouse_account, account_currency=get_account_currency(item.expense_account), + project=item.project, item=item, ) - # Loss Account (Credit) - divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount")) + # Expense Account (Debit) + if item.additional_cost_per_qty: + self.add_gl_entry( + gl_entries=gl_entries, + account=item.expense_account, + cost_center=self.cost_center or self.get_company_default("cost_center"), + debit=item.qty * item.additional_cost_per_qty, + credit=0.0, + remarks=remarks, + against_account=None, + account_currency=get_account_currency(item.expense_account), + ) - if divisional_loss: - if self.is_return: - loss_account = expenses_included_in_valuation - else: - loss_account = item.expense_account + if divisional_loss := flt(item.amount - stock_value_diff, item.precision("amount")): + loss_account = self.get_company_default( + "stock_adjustment_account", ignore_validation=True + ) + # Loss Account (Credit) self.add_gl_entry( gl_entries=gl_entries, account=loss_account, cost_center=item.cost_center, + debit=0.0, + credit=divisional_loss, + remarks=remarks, + against_account=item.expense_account, + account_currency=get_account_currency(loss_account), + project=item.project, + item=item, + ) + # Expense Account (Debit) + self.add_gl_entry( + gl_entries=gl_entries, + account=item.expense_account, + cost_center=item.cost_center, debit=divisional_loss, credit=0.0, remarks=remarks, - against_account=warehouse_account_name, - account_currency=get_account_currency(loss_account), + against_account=loss_account, + account_currency=get_account_currency(item.expense_account), project=item.project, item=item, ) @@ -596,7 +626,6 @@ class SubcontractingReceipt(SubcontractingController): ): warehouse_with_no_account.append(item.warehouse) - # Additional Costs Expense Accounts (Credit) for row in self.additional_costs: credit_amount = ( flt(row.base_amount) @@ -604,6 +633,7 @@ class SubcontractingReceipt(SubcontractingController): else flt(row.amount) ) + # Additional Cost Expense Account (Credit) self.add_gl_entry( gl_entries=gl_entries, account=row.expense_account, @@ -612,6 +642,7 @@ class SubcontractingReceipt(SubcontractingController): credit=credit_amount, remarks=remarks, against_account=None, + account_currency=get_account_currency(row.expense_account), ) if warehouse_with_no_account: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 564096ff0fe..996a99065bb 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -10,6 +10,7 @@ from frappe.utils import add_days, cint, flt, nowtime, today import erpnext from erpnext.accounts.doctype.account.test_account import get_inventory_account +from erpnext.accounts.utils import get_company_default from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.controllers.tests.test_subcontracting_controller import ( get_rm_items, @@ -351,26 +352,15 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(scr.company)), 1) gl_entries = get_gl_entries("Subcontracting Receipt", scr.name) - self.assertTrue(gl_entries) fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse) - supplier_warehouse_ac = get_inventory_account(scr.company, scr.supplier_warehouse) expense_account = scr.items[0].expense_account - - if fg_warehouse_ac == supplier_warehouse_ac: - expected_values = { - fg_warehouse_ac: [2100.0, 1000.0], # FG Amount (D), RM Cost (C) - expense_account: [0.0, 1000.0], # Service Cost (C) - additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C) - } - else: - expected_values = { - fg_warehouse_ac: [2100.0, 0.0], # FG Amount (D) - supplier_warehouse_ac: [0.0, 1000.0], # RM Cost (C) - expense_account: [0.0, 1000.0], # Service Cost (C) - additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C) - } + expected_values = { + fg_warehouse_ac: [2100.0, 1000], + expense_account: [1100, 2100], + additional_costs_expense_account: [0.0, 100.0], + } for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.debit) @@ -381,6 +371,53 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name)) frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) + @change_settings("Stock Settings", {"use_serial_batch_fields": 0}) + def test_subcontracting_receipt_with_zero_service_cost(self): + warehouse = "Stores - TCP1" + service_items = [ + { + "warehouse": warehouse, + "item_code": "Subcontracted Service Item 7", + "qty": 10, + "rate": 0, + "fg_item": "Subcontracted Item SA7", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order( + company="_Test Company with perpetual inventory", + warehouse=warehouse, + supplier_warehouse="Work In Progress - TCP1", + service_items=service_items, + ) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + + gl_entries = get_gl_entries("Subcontracting Receipt", scr.name) + self.assertTrue(gl_entries) + + fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse) + expense_account = scr.items[0].expense_account + expected_values = { + fg_warehouse_ac: [1000, 1000], + expense_account: [1000, 1000], + } + + for gle in gl_entries: + self.assertEqual(expected_values[gle.account][0], gle.debit) + self.assertEqual(expected_values[gle.account][1], gle.credit) + + scr.reload() + scr.cancel() + def test_supplied_items_consumed_qty(self): # Set Backflush Based On as "Material Transferred for Subcontracting" to transfer RM's more than the required qty set_backflush_based_on("Material Transferred for Subcontract") From 4dbeabc8efaca892be558783322769c3f41c564e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 12 Apr 2024 16:51:11 +0530 Subject: [PATCH 07/22] refactor: add payment request to dimension list (cherry picked from commit e93b4a1f2ccede787e68b78ad0b569ac735094ca) --- erpnext/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 3643afb2b13..64cbc297d20 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -540,6 +540,7 @@ accounting_dimension_doctypes = [ "Supplier Quotation Item", "Payment Reconciliation", "Payment Reconciliation Allocation", + "Payment Request", ] get_matching_queries = ( From 2d94ea074c66d430bd540a8bfa69fa2d8d76ecd5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 12 Apr 2024 17:19:58 +0530 Subject: [PATCH 08/22] chore: patch to setup exiting dimensions in Payment Request (cherry picked from commit 3f8d785f021ed47351d230fd9382c934b8e18f7f) --- erpnext/patches.txt | 1 + .../create_accounting_dimensions_in_payment_request.py | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 erpnext/patches/v15_0/create_accounting_dimensions_in_payment_request.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9fffbae4c8b..c74a2839958 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -355,6 +355,7 @@ erpnext.patches.v14_0.update_total_asset_cost_field erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22 +erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 diff --git a/erpnext/patches/v15_0/create_accounting_dimensions_in_payment_request.py b/erpnext/patches/v15_0/create_accounting_dimensions_in_payment_request.py new file mode 100644 index 00000000000..fc50b60ec9d --- /dev/null +++ b/erpnext/patches/v15_0/create_accounting_dimensions_in_payment_request.py @@ -0,0 +1,7 @@ +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + create_accounting_dimensions_for_doctype, +) + + +def execute(): + create_accounting_dimensions_for_doctype(doctype="Payment Request") From 4342b891ebdc80350c9e713c0e0a18e84713955d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 15:30:58 +0530 Subject: [PATCH 09/22] fix: type of transaction validation for the stock entry (backport #40986) (#40992) * fix: type of transaction validation for the stock entry (cherry picked from commit 8ad0295f1b00f56a345c75751936c398afd101f8) * chore: fix test case --------- Co-authored-by: Rohit Waghchaure --- .../stock/doctype/stock_entry/stock_entry.py | 46 +++++++++++++++++-- .../doctype/stock_entry/test_stock_entry.py | 35 ++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 6bcade1acae..9c94786c803 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -9,7 +9,17 @@ import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum -from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate +from frappe.utils import ( + cint, + comma_or, + cstr, + flt, + format_time, + formatdate, + get_link_to_form, + getdate, + nowdate, +) import erpnext from erpnext.accounts.general_ledger import process_gl_map @@ -640,8 +650,8 @@ class StockEntry(StockController): ) ) - work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order) - job_card_link = frappe.utils.get_link_to_form("Job Card", job_card) + work_order_link = get_link_to_form("Work Order", self.work_order) + job_card_link = get_link_to_form("Job Card", job_card) frappe.throw( _( "Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}." @@ -1350,9 +1360,24 @@ class StockEntry(StockController): return finished_item_row + def validate_serial_batch_bundle_type(self, serial_and_batch_bundle): + if ( + frappe.db.get_value("Serial and Batch Bundle", serial_and_batch_bundle, "type_of_transaction") + != "Outward" + ): + frappe.throw( + _( + "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}" + ).format(get_link_to_form("Serial and Batch Bundle", serial_and_batch_bundle)), + title=_("Invalid Serial and Batch Bundle"), + ) + def get_sle_for_source_warehouse(self, sl_entries, finished_item_row): for d in self.get("items"): if cstr(d.s_warehouse): + if d.serial_and_batch_bundle and self.docstatus == 1: + self.validate_serial_batch_bundle_type(d.serial_and_batch_bundle) + sle = self.get_sl_entries( d, { @@ -1369,6 +1394,21 @@ class StockEntry(StockController): ): sle.dependant_sle_voucher_detail_no = finished_item_row.name + if sle.serial_and_batch_bundle and self.docstatus == 2: + bundle_id = frappe.get_cached_value( + "Serial and Batch Bundle", + { + "voucher_detail_no": d.name, + "voucher_no": self.name, + "is_cancelled": 0, + "type_of_transaction": "Outward", + }, + "name", + ) + + if bundle_id: + sle.serial_and_batch_bundle = bundle_id + sl_entries.append(sle) def make_serial_and_batch_bundle_for_transfer(self): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 6c4c825c072..a680b7733d3 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1745,6 +1745,41 @@ class TestStockEntry(FrappeTestCase): self.assertTrue(frappe.db.exists("Serial No", serial_no)) self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered") + def test_serial_batch_bundle_type_of_transaction(self): + item = make_item( + "Test Use Serial and Batch Item SN Item", + { + "has_batch_no": 1, + "is_stock_item": 1, + "create_new_batch": 1, + "batch_naming_series": "Test-SBBTYT-NNS.#####", + }, + ).name + + se = make_stock_entry( + item_code=item, + qty=2, + target="_Test Warehouse - _TC", + use_serial_batch_fields=1, + ) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + se = make_stock_entry( + item_code=item, + qty=2, + source="_Test Warehouse - _TC", + target="Stores - _TC", + use_serial_batch_fields=0, + batch_no=batch_no, + do_not_submit=True, + ) + + se.reload() + sbb = se.items[0].serial_and_batch_bundle + frappe.db.set_value("Serial and Batch Bundle", sbb, "type_of_transaction", "Inward") + self.assertRaises(frappe.ValidationError, se.submit) + def make_serialized_item(**args): args = frappe._dict(args) From fea906b883060fecaaf7fbdc95f5c1a97ea5d847 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 4 Apr 2024 15:25:46 +0530 Subject: [PATCH 10/22] fix: Don't set delivery date as today while making SO from Quotation (cherry picked from commit fec20decc109ff1965fa2afe592f57113482cd73) --- erpnext/selling/doctype/quotation/quotation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 62cca80837e..f9442204df1 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -384,7 +384,6 @@ def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_ ) target.flags.ignore_permissions = ignore_permissions - target.delivery_date = nowdate() target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") From 0ee91a2e541b59146a333fde7248c1e2eee2a797 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 11 Apr 2024 11:01:28 +0530 Subject: [PATCH 11/22] fix: test cases (cherry picked from commit 65c74fa3c7197f0e751ba62abe1f63c8e7735147) --- erpnext/selling/doctype/quotation/test_quotation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index b901240bcdb..7b66b7a251a 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -122,6 +122,7 @@ class TestQuotation(FrappeTestCase): sales_order.naming_series = "_T-Quotation-" sales_order.transaction_date = nowdate() + sales_order.delivery_date = nowdate() sales_order.insert() def test_make_sales_order_with_terms(self): @@ -152,6 +153,7 @@ class TestQuotation(FrappeTestCase): sales_order.naming_series = "_T-Quotation-" sales_order.transaction_date = nowdate() + sales_order.delivery_date = nowdate() sales_order.insert() # Remove any unknown taxes if applied From 1c28ed4d5b570bc64fe45ea7b7dffda7bb264f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CKhushi?= <“khushirawat.sophia@gamil.com”> Date: Wed, 27 Mar 2024 16:25:13 +0530 Subject: [PATCH 12/22] fix: voucher no. is link field for non english user interface (cherry picked from commit 2b8928cae6221859f1489961788b85ce6314d0ae) --- erpnext/accounts/report/general_ledger/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 9c5b3af0f44..02de6c3aee1 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -460,7 +460,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): for gle in gl_entries: group_by_value = gle.get(group_by) - gle.voucher_type = _(gle.voucher_type) + gle.voucher_type = gle.voucher_type if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries): if not group_by_voucher_consolidated: From e287376cc8daff44149a34bb552aeb94886d130e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 1 Apr 2024 21:21:46 +0530 Subject: [PATCH 13/22] fix: Multiple partial payment requests against Purchase Invoice (cherry picked from commit 45d5f6e00a231be0511ff541df6d778b3c2a347e) --- .../payment_request/payment_request.py | 25 +++++----- .../payment_request/test_payment_request.py | 47 +++++++++++++++++++ 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 3233e0a5bec..d92cbaebf74 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -330,21 +330,12 @@ class PaymentRequest(Document): } ) + payment_entry.received_amount = payment_entry.base_paid_amount + payment_entry.get("references")[0].allocated_amount = payment_entry.base_paid_amount + for dimension in get_accounting_dimensions(): payment_entry.update({dimension: self.get(dimension)}) - if payment_entry.difference_amount: - company_details = get_company_defaults(ref_doc.company) - - payment_entry.append( - "deductions", - { - "account": company_details.exchange_gain_loss_account, - "cost_center": company_details.cost_center, - "amount": payment_entry.difference_amount, - }, - ) - if submit: payment_entry.insert(ignore_permissions=True) payment_entry.submit() @@ -463,6 +454,12 @@ def make_payment_request(**args): pr = frappe.get_doc("Payment Request", draft_payment_request) else: pr = frappe.new_doc("Payment Request") + + if not args.get("payment_request_type"): + args["payment_request_type"] = ( + "Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward" + ) + pr.update( { "payment_gateway_account": gateway_account.get("name"), @@ -521,9 +518,9 @@ def get_amount(ref_doc, payment_account=None): elif dt in ["Sales Invoice", "Purchase Invoice"]: if not ref_doc.get("is_pos"): if ref_doc.party_account_currency == ref_doc.currency: - grand_total = flt(ref_doc.outstanding_amount) + grand_total = flt(ref_doc.grand_total) else: - grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate + grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate elif dt == "Sales Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 70de886ba4d..932060895b0 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -86,6 +86,8 @@ class TestPaymentRequest(unittest.TestCase): pr = make_payment_request( dt="Purchase Invoice", dn=si_usd.name, + party_type="Supplier", + party="_Test Supplier USD", recipient_id="user@example.com", mute_email=1, payment_gateway_account="_Test Gateway - USD", @@ -98,6 +100,51 @@ class TestPaymentRequest(unittest.TestCase): self.assertEqual(pr.status, "Paid") + def test_multiple_payment_entry_against_purchase_invoice(self): + purchase_invoice = make_purchase_invoice( + customer="_Test Supplier USD", + debit_to="_Test Payable USD - _TC", + currency="USD", + conversion_rate=50, + ) + + pr = make_payment_request( + dt="Purchase Invoice", + party_type="Supplier", + party="_Test Supplier USD", + dn=purchase_invoice.name, + recipient_id="user@example.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + return_doc=1, + ) + + pr.grand_total = pr.grand_total / 2 + + pr.submit() + pr.create_payment_entry() + + purchase_invoice.load_from_db() + self.assertEqual(purchase_invoice.status, "Partly Paid") + + pr = make_payment_request( + dt="Purchase Invoice", + party_type="Supplier", + party="_Test Supplier USD", + dn=purchase_invoice.name, + recipient_id="user@example.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + return_doc=1, + ) + + pr.save() + pr.submit() + pr.create_payment_entry() + + purchase_invoice.load_from_db() + self.assertEqual(purchase_invoice.status, "Paid") + def test_payment_entry(self): frappe.db.set_value( "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" From 641b2a4705e8387500e39adecce3576f15ef9645 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 15 Apr 2024 20:49:40 +0530 Subject: [PATCH 14/22] fix: Test case (cherry picked from commit 071e5ed648b3627ac5594aecb7601f6909540e1f) --- .../doctype/payment_request/payment_request.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index d92cbaebf74..03435a26011 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -91,7 +91,7 @@ class PaymentRequest(Document): self.status = "Draft" self.validate_reference_document() self.validate_payment_request_amount() - self.validate_currency() + # self.validate_currency() self.validate_subscription_details() def validate_reference_document(self): @@ -330,8 +330,13 @@ class PaymentRequest(Document): } ) - payment_entry.received_amount = payment_entry.base_paid_amount - payment_entry.get("references")[0].allocated_amount = payment_entry.base_paid_amount + if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: + amount = payment_entry.base_paid_amount + else: + amount = self.grand_total + + payment_entry.received_amount = amount + payment_entry.get("references")[0].allocated_amount = amount for dimension in get_accounting_dimensions(): payment_entry.update({dimension: self.get(dimension)}) From 361d7f1ba539f670b87dfb00493d6a165b4d5c7c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 16 Apr 2024 08:53:29 +0530 Subject: [PATCH 15/22] fix: incorrect exc gain/loss for PE against JE for payable accounts (cherry picked from commit 81b574053ffc15c4337fbe497c53d8696d26f878) --- erpnext/controllers/accounts_controller.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 884312f75c6..f161583458b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1437,7 +1437,8 @@ class AccountsController(TransactionBase): dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" - if d.reference_doctype == "Purchase Invoice": + # Inverse debit/credit for payable accounts + if self.is_payable_account(d.reference_doctype, party_account): dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" @@ -1471,6 +1472,14 @@ class AccountsController(TransactionBase): ) ) + def is_payable_account(self, reference_doctype, account): + if reference_doctype == "Purchase Invoice" or ( + reference_doctype == "Journal Entry" + and frappe.get_cached_value("Account", account, "account_type") == "Payable" + ): + return True + return False + def update_against_document_in_jv(self): """ Links invoice and advance voucher: From 59950290a24d2c53c55418d47e63f971243a0c3c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 16 Apr 2024 10:17:41 +0530 Subject: [PATCH 16/22] test: exc gain/loss journals booking in Payable accounts (cherry picked from commit 8821c98625f21fe2ec3434462c031f8bd27afde7) --- .../tests/test_accounts_controller.py | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 6218bd6ddf0..f91acb2e4ea 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -135,6 +135,27 @@ class TestAccountsController(FrappeTestCase): acc = frappe.get_doc("Account", name) self.debtors_usd = acc.name + account_name = "Creditors USD" + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = account_name + acc.parent_account = "Accounts Payable - " + self.company_abbr + acc.company = self.company + acc.account_currency = "USD" + acc.account_type = "Payable" + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + self.creditors_usd = acc.name + def create_sales_invoice( self, qty=1, @@ -174,7 +195,9 @@ class TestAccountsController(FrappeTestCase): ) return sinv - def create_payment_entry(self, amount=1, source_exc_rate=75, posting_date=None, customer=None): + def create_payment_entry( + self, amount=1, source_exc_rate=75, posting_date=None, customer=None, submit=True + ): """ Helper function to populate default values in payment entry """ @@ -1606,3 +1629,72 @@ class TestAccountsController(FrappeTestCase): exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name) self.assertEqual(exc_je_for_je1, []) self.assertEqual(exc_je_for_je2, []) + + def test_61_payment_entry_against_journal_for_payable_accounts(self): + # Invoices + exc_rate1 = 75 + exc_rate2 = 77 + amount = 1 + je1 = self.create_journal_entry( + acc1=self.creditors_usd, + acc1_exc_rate=exc_rate1, + acc2=self.cash, + acc1_amount=-amount, + acc2_amount=(-amount * 75), + acc2_exc_rate=1, + ) + je1.accounts[0].party_type = "Supplier" + je1.accounts[0].party = self.supplier + je1 = je1.save().submit() + + # Payment + pe = create_payment_entry( + company=self.company, + payment_type="Pay", + party_type="Supplier", + party=self.supplier, + paid_from=self.cash, + paid_to=self.creditors_usd, + paid_amount=amount, + ) + pe.target_exchange_rate = exc_rate2 + pe.received_amount = amount + pe.paid_amount = amount * exc_rate2 + pe.save().submit() + + pr = frappe.get_doc( + { + "doctype": "Payment Reconciliation", + "company": self.company, + "party_type": "Supplier", + "party": self.supplier, + "receivable_payable_account": get_party_account("Supplier", self.supplier, self.company), + } + ) + pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() + 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 = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be no outstanding in both currencies + self.assert_ledger_outstanding(je1.doctype, je1.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created + exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) + self.assertEqual(len(exc_je_for_je1), 1) + + # Cancel Payment + pe.reload() + pe.cancel() + + self.assert_ledger_outstanding(je1.doctype, je1.name, (amount * exc_rate1), amount) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) + self.assertEqual(exc_je_for_je1, []) From f59e43320b6bfc58935f8383850d46c5e8e8bdc9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:22:43 +0530 Subject: [PATCH 17/22] fix: expense account set as COGS for stock entry Material Issue (backport #41026) (#41029) fix: expense account set as COGS for stock entry Material Issue (#41026) (cherry picked from commit 03231e99ef638c2938eb570eb184bb8a8f1dcdca) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/stock_entry/stock_entry.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 9c94786c803..0ba1b56272d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1641,11 +1641,7 @@ class StockEntry(StockController): ret.update(get_uom_details(args.get("item_code"), args.get("uom"), args.get("qty"))) if self.purpose == "Material Issue": - ret["expense_account"] = ( - item.get("expense_account") - or item_group_defaults.get("expense_account") - or frappe.get_cached_value("Company", self.company, "default_expense_account") - ) + ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account") for company_field, field in { "stock_adjustment_account": "expense_account", From ccdbad9f90bde08b702e5749b9c2ab0ac98b0655 Mon Sep 17 00:00:00 2001 From: Nihantra Patel Date: Mon, 11 Mar 2024 16:34:44 +0530 Subject: [PATCH 18/22] fix: get address if multiple companies (cherry picked from commit c6cf1bec76581bbe653b191e983de1692254ef48) --- .../doctype/sales_order/sales_order.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 2ee7543b3c0..dfc414ada40 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -169,6 +169,27 @@ frappe.ui.form.on("Sales Order", { ); }, + // When multiple companies are set up. in case company name is changed set default company address + company: function (frm) { + if (frm.doc.company) { + frappe.call({ + method: "erpnext.setup.doctype.company.company.get_default_company_address", + args: { + name: frm.doc.company, + existing_address: frm.doc.company_address || "" + }, + debounce: 2000, + callback: function (r) { + if (r.message) { + frm.set_value("company_address", r.message); + } else { + frm.set_value("company_address", ""); + } + }, + }); + } + }, + onload: function (frm) { if (!frm.doc.transaction_date) { frm.set_value("transaction_date", frappe.datetime.get_today()); From dcfc768d33eac293bf7435bcce29d64039f1e06b Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <141945075+Nihantra-Patel@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:46:00 +0530 Subject: [PATCH 19/22] fix: get address if multiple companies (cherry picked from commit 655a1797bed38f87172aeb4589bc16c6490327a4) --- erpnext/selling/doctype/sales_order/sales_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index dfc414ada40..d95bf9811a2 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -176,7 +176,7 @@ frappe.ui.form.on("Sales Order", { method: "erpnext.setup.doctype.company.company.get_default_company_address", args: { name: frm.doc.company, - existing_address: frm.doc.company_address || "" + existing_address: frm.doc.company_address || "", }, debounce: 2000, callback: function (r) { From 40d059c7c1f3db7ad53e6947d1cf090ced3a4932 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:24:38 +0530 Subject: [PATCH 20/22] fix: Delayed Order Report not working (backport #41037) (#41039) fix: Delayed Order Report not working (#41037) (cherry picked from commit d69a18b8262bdc77b9fc0cb7eabb6321626b1926) Co-authored-by: rohitwaghchaure --- .../stock/report/delayed_item_report/delayed_item_report.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/delayed_item_report/delayed_item_report.py b/erpnext/stock/report/delayed_item_report/delayed_item_report.py index 0bfb4da06a0..88a188e0cad 100644 --- a/erpnext/stock/report/delayed_item_report/delayed_item_report.py +++ b/erpnext/stock/report/delayed_item_report/delayed_item_report.py @@ -86,7 +86,11 @@ class DelayedItemReport: filters = {"parent": ("in", sales_orders), "name": ("in", sales_order_items)} so_data = {} - for d in frappe.get_all(doctype, filters=filters, fields=["delivery_date", "parent", "name"]): + fields = ["delivery_date", "name"] + if frappe.db.has_column(doctype, "parent"): + fields.append("parent") + + for d in frappe.get_all(doctype, filters=filters, fields=fields): key = d.name if consolidated else (d.parent, d.name) if key not in so_data: so_data.setdefault(key, d.delivery_date) From b27ad765576dbb06b9b160939f769d94ea6ccb90 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 06:37:38 +0530 Subject: [PATCH 21/22] fix: not able to submit subcontracting receipt (backport #41041) (#41045) fix: not able to submit subcontracting receipt (#41041) (cherry picked from commit 5b1493b56c72ec8890adf9518ecd700941055c9c) Co-authored-by: rohitwaghchaure --- .../doctype/subcontracting_receipt/subcontracting_receipt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index cc800e4369d..19e8dfd5431 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -722,6 +722,7 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False "purchase_order": item.purchase_order, "purchase_order_item": item.purchase_order_item, "subcontracting_receipt_item": item.name, + "project": po_item.project, } target_doc.append("items", item_row) From 881dc0234944f2ce6e0c928f066f45b98eeaa3c3 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Sat, 30 Mar 2024 16:32:43 +0530 Subject: [PATCH 22/22] fix(gp): SLEs not fetched for correct warehouse (cherry picked from commit f958e8be0698723630db0c742cb6fe9428c2c8f4) --- erpnext/accounts/report/gross_profit/gross_profit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2b851050639..432d81847e3 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -656,13 +656,13 @@ class GrossProfitGenerator(object): elif self.delivery_notes.get((row.parent, row.item_code), None): # check if Invoice has delivery notes dn = self.delivery_notes.get((row.parent, row.item_code)) - parenttype, parent, item_row, warehouse = ( + parenttype, parent, item_row, dn_warehouse = ( "Delivery Note", dn["delivery_note"], dn["item_row"], dn["warehouse"], ) - my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) + my_sle = self.get_stock_ledger_entries(item_code, dn_warehouse) return self.calculate_buying_amount_from_sle( row, my_sle, parenttype, parent, item_row, item_code )