From 7d871f6bb54bccb4615d414cef82c78ba21c5302 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 6 Feb 2025 14:46:19 +0530 Subject: [PATCH 01/58] fix: stock reservation not working for sales invoice with update stock (cherry picked from commit 0c9d0ea1f4a7731706fb97d17e8c76f49f2e6477) # Conflicts: # erpnext/selling/doctype/sales_order/test_sales_order.py --- .../doctype/sales_invoice/sales_invoice.py | 3 + erpnext/controllers/selling_controller.py | 145 ++++++++++++++++++ .../doctype/sales_order/test_sales_order.py | 117 ++++++++++++++ .../doctype/delivery_note/delivery_note.py | 143 ----------------- 4 files changed, 265 insertions(+), 143 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 5753eba8cc1..7345a5ef78d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -460,6 +460,8 @@ class SalesInvoice(SellingController): self.make_bundle_for_sales_purchase_return(table_name) self.make_bundle_using_old_serial_batch_fields(table_name) + + self.update_stock_reservation_entries() self.update_stock_ledger() # this sequence because outstanding may get -ve @@ -561,6 +563,7 @@ class SalesInvoice(SellingController): self.make_gl_entries_on_cancel() if self.update_stock == 1: + self.update_stock_reservation_entries() self.repost_future_sle_and_gle() self.db_set("status", "Cancelled") diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index f79d83d5b09..4b5b28c05fa 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -791,6 +791,151 @@ class SellingController(StockController): validate_item_type(self, "is_sales_item", "sales") + def update_stock_reservation_entries(self) -> None: + """Updates Delivered Qty in Stock Reservation Entries.""" + + # Don't update Delivered Qty on Return. + if self.is_return: + return + + so_field = "sales_order" if self.doctype == "Sales Invoice" else "against_sales_order" + + if self._action == "submit": + for item in self.get("items"): + # Skip if `Sales Order` or `Sales Order Item` reference is not set. + if not item.get(so_field) or not item.so_detail: + continue + + sre_list = frappe.db.get_all( + "Stock Reservation Entry", + { + "docstatus": 1, + "voucher_type": "Sales Order", + "voucher_no": item.get(so_field), + "voucher_detail_no": item.so_detail, + "warehouse": item.warehouse, + "status": ["not in", ["Delivered", "Cancelled"]], + }, + order_by="creation", + ) + + # Skip if no Stock Reservation Entries. + if not sre_list: + continue + + qty_to_deliver = item.stock_qty + for sre in sre_list: + if qty_to_deliver <= 0: + break + + sre_doc = frappe.get_doc("Stock Reservation Entry", sre) + + qty_can_be_deliver = 0 + if sre_doc.reservation_based_on == "Serial and Batch": + sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) + if sre_doc.has_serial_no: + delivered_serial_nos = [d.serial_no for d in sbb.entries] + for entry in sre_doc.sb_entries: + if entry.serial_no in delivered_serial_nos: + entry.delivered_qty = 1 # Qty will always be 0 or 1 for Serial No. + entry.db_update() + qty_can_be_deliver += 1 + delivered_serial_nos.remove(entry.serial_no) + else: + delivered_batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries} + for entry in sre_doc.sb_entries: + if entry.batch_no in delivered_batch_qty: + delivered_qty = min( + (entry.qty - entry.delivered_qty), delivered_batch_qty[entry.batch_no] + ) + entry.delivered_qty += delivered_qty + entry.db_update() + qty_can_be_deliver += delivered_qty + delivered_batch_qty[entry.batch_no] -= delivered_qty + else: + # `Delivered Qty` should be less than or equal to `Reserved Qty`. + qty_can_be_deliver = min( + (sre_doc.reserved_qty - sre_doc.delivered_qty), qty_to_deliver + ) + + sre_doc.delivered_qty += qty_can_be_deliver + sre_doc.db_update() + + # Update Stock Reservation Entry `Status` based on `Delivered Qty`. + sre_doc.update_status() + + # Update Reserved Stock in Bin. + sre_doc.update_reserved_stock_in_bin() + + qty_to_deliver -= qty_can_be_deliver + + if self._action == "cancel": + for item in self.get("items"): + # Skip if `Sales Order` or `Sales Order Item` reference is not set. + if not item.get(so_field) or not item.so_detail: + continue + + sre_list = frappe.db.get_all( + "Stock Reservation Entry", + { + "docstatus": 1, + "voucher_type": "Sales Order", + "voucher_no": item.get(so_field), + "voucher_detail_no": item.so_detail, + "warehouse": item.warehouse, + "status": ["in", ["Partially Delivered", "Delivered"]], + }, + order_by="creation", + ) + + # Skip if no Stock Reservation Entries. + if not sre_list: + continue + + qty_to_undelivered = item.stock_qty + for sre in sre_list: + if qty_to_undelivered <= 0: + break + + sre_doc = frappe.get_doc("Stock Reservation Entry", sre) + + qty_can_be_undelivered = 0 + if sre_doc.reservation_based_on == "Serial and Batch": + sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) + if sre_doc.has_serial_no: + serial_nos_to_undelivered = [d.serial_no for d in sbb.entries] + for entry in sre_doc.sb_entries: + if entry.serial_no in serial_nos_to_undelivered: + entry.delivered_qty = 0 # Qty will always be 0 or 1 for Serial No. + entry.db_update() + qty_can_be_undelivered += 1 + serial_nos_to_undelivered.remove(entry.serial_no) + else: + batch_qty_to_undelivered = {d.batch_no: -1 * d.qty for d in sbb.entries} + for entry in sre_doc.sb_entries: + if entry.batch_no in batch_qty_to_undelivered: + undelivered_qty = min( + entry.delivered_qty, batch_qty_to_undelivered[entry.batch_no] + ) + entry.delivered_qty -= undelivered_qty + entry.db_update() + qty_can_be_undelivered += undelivered_qty + batch_qty_to_undelivered[entry.batch_no] -= undelivered_qty + else: + # `Qty to Undelivered` should be less than or equal to `Delivered Qty`. + qty_can_be_undelivered = min(sre_doc.delivered_qty, qty_to_undelivered) + + sre_doc.delivered_qty -= qty_can_be_undelivered + sre_doc.db_update() + + # Update Stock Reservation Entry `Status` based on `Delivered Qty`. + sre_doc.update_status() + + # Update Reserved Stock in Bin. + sre_doc.update_reserved_stock_in_bin() + + qty_to_undelivered -= qty_can_be_undelivered + def set_default_income_account_for_item(obj): for d in obj.get("items"): diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 47d42b0a9d5..e4fc1b6fe53 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2119,6 +2119,123 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertRaises(frappe.ValidationError, so1.update_status, "Draft") +<<<<<<< HEAD +======= + @IntegrationTestCase.change_settings("Stock Settings", {"enable_stock_reservation": True}) + def test_warehouse_mapping_based_on_stock_reservation(self): + self.create_company(company_name="Glass Ceiling", abbr="GC") + self.create_item("Lamy Safari 2", True, self.warehouse_stores, self.company, 2000) + self.create_customer() + self.clear_old_entries() + + so = frappe.new_doc("Sales Order") + so.company = self.company + so.customer = self.customer + so.transaction_date = today() + so.append( + "items", + { + "item_code": self.item, + "qty": 10, + "rate": 2000, + "warehouse": self.warehouse_stores, + "delivery_date": today(), + }, + ) + so.submit() + + # Create stock + se = frappe.get_doc( + { + "doctype": "Stock Entry", + "company": self.company, + "stock_entry_type": "Material Receipt", + "posting_date": today(), + "items": [ + {"item_code": self.item, "t_warehouse": self.warehouse_stores, "qty": 5}, + {"item_code": self.item, "t_warehouse": self.warehouse_finished_goods, "qty": 5}, + ], + } + ) + se.submit() + + # Reserve stock on 2 different warehouses + itm = so.items[0] + so.create_stock_reservation_entries( + [ + { + "sales_order_item": itm.name, + "item_code": itm.item_code, + "warehouse": self.warehouse_stores, + "qty_to_reserve": 2, + } + ] + ) + so.create_stock_reservation_entries( + [ + { + "sales_order_item": itm.name, + "item_code": itm.item_code, + "warehouse": self.warehouse_finished_goods, + "qty_to_reserve": 3, + } + ] + ) + + # Delivery note should auto-select warehouse based on reservation + dn = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) + self.assertEqual(2, len(dn.items)) + self.assertEqual(dn.items[0].qty, 2) + self.assertEqual(dn.items[0].warehouse, self.warehouse_stores) + self.assertEqual(dn.items[1].qty, 3) + self.assertEqual(dn.items[1].warehouse, self.warehouse_finished_goods) + + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + warehouse = create_warehouse("Test Warehouse 1", company=self.company) + + make_stock_entry( + item_code=self.item, + target=warehouse, + qty=5, + company=self.company, + ) + + so = frappe.new_doc("Sales Order") + so.reserve_stock = 1 + so.company = self.company + so.customer = self.customer + so.transaction_date = today() + so.currency = "INR" + so.append( + "items", + { + "item_code": self.item, + "qty": 5, + "rate": 2000, + "warehouse": warehouse, + "delivery_date": today(), + }, + ) + so.submit() + + sres = frappe.get_all( + "Stock Reservation Entry", + filters={"voucher_no": so.name}, + fields=["name"], + ) + + self.assertEqual(len(sres), 1) + sre_doc = frappe.get_doc("Stock Reservation Entry", sres[0].name) + self.assertFalse(sre_doc.status == "Delivered") + + si = make_sales_invoice(so.name) + si.update_stock = 1 + si.submit() + sre_doc.reload() + self.assertTrue(sre_doc.status == "Delivered") + +>>>>>>> 0c9d0ea1f4 (fix: stock reservation not working for sales invoice with update stock) def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 39527c9ec47..ba04abce8f3 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -491,149 +491,6 @@ class DeliveryNote(SellingController): self.delete_auto_created_batches() - def update_stock_reservation_entries(self) -> None: - """Updates Delivered Qty in Stock Reservation Entries.""" - - # Don't update Delivered Qty on Return. - if self.is_return: - return - - if self._action == "submit": - for item in self.get("items"): - # Skip if `Sales Order` or `Sales Order Item` reference is not set. - if not item.against_sales_order or not item.so_detail: - continue - - sre_list = frappe.db.get_all( - "Stock Reservation Entry", - { - "docstatus": 1, - "voucher_type": "Sales Order", - "voucher_no": item.against_sales_order, - "voucher_detail_no": item.so_detail, - "warehouse": item.warehouse, - "status": ["not in", ["Delivered", "Cancelled"]], - }, - order_by="creation", - ) - - # Skip if no Stock Reservation Entries. - if not sre_list: - continue - - qty_to_deliver = item.stock_qty - for sre in sre_list: - if qty_to_deliver <= 0: - break - - sre_doc = frappe.get_doc("Stock Reservation Entry", sre) - - qty_can_be_deliver = 0 - if sre_doc.reservation_based_on == "Serial and Batch": - sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) - if sre_doc.has_serial_no: - delivered_serial_nos = [d.serial_no for d in sbb.entries] - for entry in sre_doc.sb_entries: - if entry.serial_no in delivered_serial_nos: - entry.delivered_qty = 1 # Qty will always be 0 or 1 for Serial No. - entry.db_update() - qty_can_be_deliver += 1 - delivered_serial_nos.remove(entry.serial_no) - else: - delivered_batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries} - for entry in sre_doc.sb_entries: - if entry.batch_no in delivered_batch_qty: - delivered_qty = min( - (entry.qty - entry.delivered_qty), delivered_batch_qty[entry.batch_no] - ) - entry.delivered_qty += delivered_qty - entry.db_update() - qty_can_be_deliver += delivered_qty - delivered_batch_qty[entry.batch_no] -= delivered_qty - else: - # `Delivered Qty` should be less than or equal to `Reserved Qty`. - qty_can_be_deliver = min( - (sre_doc.reserved_qty - sre_doc.delivered_qty), qty_to_deliver - ) - - sre_doc.delivered_qty += qty_can_be_deliver - sre_doc.db_update() - - # Update Stock Reservation Entry `Status` based on `Delivered Qty`. - sre_doc.update_status() - - # Update Reserved Stock in Bin. - sre_doc.update_reserved_stock_in_bin() - - qty_to_deliver -= qty_can_be_deliver - - if self._action == "cancel": - for item in self.get("items"): - # Skip if `Sales Order` or `Sales Order Item` reference is not set. - if not item.against_sales_order or not item.so_detail: - continue - - sre_list = frappe.db.get_all( - "Stock Reservation Entry", - { - "docstatus": 1, - "voucher_type": "Sales Order", - "voucher_no": item.against_sales_order, - "voucher_detail_no": item.so_detail, - "warehouse": item.warehouse, - "status": ["in", ["Partially Delivered", "Delivered"]], - }, - order_by="creation", - ) - - # Skip if no Stock Reservation Entries. - if not sre_list: - continue - - qty_to_undelivered = item.stock_qty - for sre in sre_list: - if qty_to_undelivered <= 0: - break - - sre_doc = frappe.get_doc("Stock Reservation Entry", sre) - - qty_can_be_undelivered = 0 - if sre_doc.reservation_based_on == "Serial and Batch": - sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) - if sre_doc.has_serial_no: - serial_nos_to_undelivered = [d.serial_no for d in sbb.entries] - for entry in sre_doc.sb_entries: - if entry.serial_no in serial_nos_to_undelivered: - entry.delivered_qty = 0 # Qty will always be 0 or 1 for Serial No. - entry.db_update() - qty_can_be_undelivered += 1 - serial_nos_to_undelivered.remove(entry.serial_no) - else: - batch_qty_to_undelivered = {d.batch_no: -1 * d.qty for d in sbb.entries} - for entry in sre_doc.sb_entries: - if entry.batch_no in batch_qty_to_undelivered: - undelivered_qty = min( - entry.delivered_qty, batch_qty_to_undelivered[entry.batch_no] - ) - entry.delivered_qty -= undelivered_qty - entry.db_update() - qty_can_be_undelivered += undelivered_qty - batch_qty_to_undelivered[entry.batch_no] -= undelivered_qty - else: - # `Qty to Undelivered` should be less than or equal to `Delivered Qty`. - qty_can_be_undelivered = min(sre_doc.delivered_qty, qty_to_undelivered) - - sre_doc.delivered_qty -= qty_can_be_undelivered - sre_doc.db_update() - - # Update Stock Reservation Entry `Status` based on `Delivered Qty`. - sre_doc.update_status() - - # Update Reserved Stock in Bin. - sre_doc.update_reserved_stock_in_bin() - - qty_to_undelivered -= qty_can_be_undelivered - def validate_against_stock_reservation_entries(self): """Validates if Stock Reservation Entries are available for the Sales Order Item reference.""" From 4889950a9e6ec2007be655ed5037c884ca370d68 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 6 Feb 2025 15:46:07 +0530 Subject: [PATCH 02/58] chore: fix conflicts --- erpnext/selling/doctype/sales_order/test_sales_order.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index e4fc1b6fe53..99912147cad 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2119,10 +2119,8 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertRaises(frappe.ValidationError, so1.update_status, "Draft") -<<<<<<< HEAD -======= - @IntegrationTestCase.change_settings("Stock Settings", {"enable_stock_reservation": True}) def test_warehouse_mapping_based_on_stock_reservation(self): + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", True) self.create_company(company_name="Glass Ceiling", abbr="GC") self.create_item("Lamy Safari 2", True, self.warehouse_stores, self.company, 2000) self.create_customer() @@ -2235,7 +2233,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): sre_doc.reload() self.assertTrue(sre_doc.status == "Delivered") ->>>>>>> 0c9d0ea1f4 (fix: stock reservation not working for sales invoice with update stock) def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") From 162d1ba472c4a81ac7f7234c16a4159abf0eeb74 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 6 Feb 2025 22:13:26 +0530 Subject: [PATCH 03/58] chore: fix test case --- erpnext/selling/doctype/sales_order/test_sales_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 99912147cad..72cba2a1e9d 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2122,7 +2122,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): def test_warehouse_mapping_based_on_stock_reservation(self): frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", True) self.create_company(company_name="Glass Ceiling", abbr="GC") - self.create_item("Lamy Safari 2", True, self.warehouse_stores, self.company, 2000) + self.create_item("Lamy Safari 2", True, self.warehouse_stores, self.company) self.create_customer() self.clear_old_entries() @@ -2186,7 +2186,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertEqual(dn.items[0].qty, 2) self.assertEqual(dn.items[0].warehouse, self.warehouse_stores) self.assertEqual(dn.items[1].qty, 3) - self.assertEqual(dn.items[1].warehouse, self.warehouse_finished_goods) from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -2196,6 +2195,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): item_code=self.item, target=warehouse, qty=5, + rate=200, company=self.company, ) From 7e85a123b243091eaee83ee59b3bcb21aa69aa76 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Sat, 8 Feb 2025 22:49:35 +0530 Subject: [PATCH 04/58] fix(report): add options to multiselectlist fields (cherry picked from commit 8785342fcee1c80662ab1a2bcf06280b69979fe5) # Conflicts: # erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js # erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js --- .../accounts_payable/accounts_payable.js | 1 + .../accounts_payable_summary.js | 1 + .../accounts_receivable.js | 1 + .../accounts_receivable_summary.js | 1 + .../budget_variance_report.js | 1 + .../customer_ledger_summary.js | 25 +++++++++++++++++++ .../report/general_ledger/general_ledger.js | 3 +++ .../report/gross_profit/gross_profit.js | 2 ++ .../report/payment_ledger/payment_ledger.js | 1 + .../supplier_ledger_summary.js | 25 +++++++++++++++++++ .../purchase_order_analysis.js | 1 + .../supplier_quotation_comparison.js | 2 ++ .../opportunity_summary_by_sales_stage.js | 1 + .../job_card_summary/job_card_summary.js | 2 ++ .../production_planning_report.js | 2 +- .../work_order_summary/work_order_summary.js | 2 ++ .../payment_terms_status_for_sales_order.js | 1 + .../sales_order_analysis.js | 1 + .../item_shortage_report.js | 1 + .../serial_and_batch_summary.js | 1 + 20 files changed, 74 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 445e532183b..c13197613d2 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -89,6 +89,7 @@ frappe.query_reports["Accounts Payable"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index cf7a62c6b69..e46af2657b5 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -66,6 +66,7 @@ frappe.query_reports["Accounts Payable Summary"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 9f15bbc333d..01f5a205cea 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -56,6 +56,7 @@ frappe.query_reports["Accounts Receivable"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index e36f40169b3..17ee5e0b323 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -66,6 +66,7 @@ frappe.query_reports["Accounts Receivable Summary"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 83bd48c71f3..c74450191aa 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -91,6 +91,7 @@ function get_filters() { fieldname: "budget_against_filter", label: __("Dimension Filter"), fieldtype: "MultiSelectList", + options: "budget_against", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js index 3600db852f8..771133df063 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js @@ -92,5 +92,30 @@ frappe.query_reports["Customer Ledger Summary"] = { fieldtype: "Data", hidden: 1, }, +<<<<<<< HEAD +======= + { + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "MultiSelectList", + options: "Cost Center", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); + }, + }, + { + fieldname: "project", + label: __("Project"), + fieldtype: "MultiSelectList", + options: "Project", + get_data: function (txt) { + return frappe.db.get_link_options("Project", txt, { + company: frappe.query_report.get_filter_value("company"), + }); + }, + }, +>>>>>>> 8785342fce (fix(report): add options to multiselectlist fields) ], }; diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index e202d5dfffd..54d6fb2e2f6 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -73,6 +73,7 @@ frappe.query_reports["General Ledger"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; @@ -151,6 +152,7 @@ frappe.query_reports["General Ledger"] = { fieldname: "cost_center", label: __("Cost Center"), fieldtype: "MultiSelectList", + options: "Cost Center", get_data: function (txt) { return frappe.db.get_link_options("Cost Center", txt, { company: frappe.query_report.get_filter_value("company"), @@ -161,6 +163,7 @@ frappe.query_reports["General Ledger"] = { fieldname: "project", label: __("Project"), fieldtype: "MultiSelectList", + options: "Project", get_data: function (txt) { return frappe.db.get_link_options("Project", txt, { company: frappe.query_report.get_filter_value("company"), diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index ad194ee90a2..0ddb95fff2f 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -67,6 +67,7 @@ frappe.query_reports["Gross Profit"] = { fieldname: "cost_center", label: __("Cost Center"), fieldtype: "MultiSelectList", + options: "Cost Center", get_data: function (txt) { return frappe.db.get_link_options("Cost Center", txt, { company: frappe.query_report.get_filter_value("company"), @@ -77,6 +78,7 @@ frappe.query_reports["Gross Profit"] = { fieldname: "project", label: __("Project"), fieldtype: "MultiSelectList", + options: "Project", get_data: function (txt) { return frappe.db.get_link_options("Project", txt, { company: frappe.query_report.get_filter_value("company"), diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.js b/erpnext/accounts/report/payment_ledger/payment_ledger.js index 8d1f227fe6d..7ffd26c9700 100644 --- a/erpnext/accounts/report/payment_ledger/payment_ledger.js +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.js @@ -50,6 +50,7 @@ function get_filters() { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js index 5d91575b8b2..7d031f88ecc 100644 --- a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js +++ b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js @@ -74,5 +74,30 @@ frappe.query_reports["Supplier Ledger Summary"] = { fieldtype: "Data", hidden: 1, }, +<<<<<<< HEAD +======= + { + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "MultiSelectList", + options: "Cost Center", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); + }, + }, + { + fieldname: "project", + label: __("Project"), + fieldtype: "MultiSelectList", + options: "Project", + get_data: function (txt) { + return frappe.db.get_link_options("Project", txt, { + company: frappe.query_report.get_filter_value("company"), + }); + }, + }, +>>>>>>> 8785342fce (fix(report): add options to multiselectlist fields) ], }; diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js index d8c91babdfd..58657da9168 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js @@ -52,6 +52,7 @@ frappe.query_reports["Purchase Order Analysis"] = { label: __("Status"), fieldtype: "MultiSelectList", width: "80", + options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"], get_data: function (txt) { let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"]; let options = []; diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index 9701e147f05..2c0be8f70d7 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -50,6 +50,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = { fieldname: "supplier", label: __("Supplier"), fieldtype: "MultiSelectList", + options: "Supplier", get_data: function (txt) { return frappe.db.get_link_options("Supplier", txt); }, @@ -58,6 +59,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = { fieldtype: "MultiSelectList", label: __("Supplier Quotation"), fieldname: "supplier_quotation", + options: "Supplier Quotation", default: "", get_data: function (txt) { return frappe.db.get_link_options("Supplier Quotation", txt, { docstatus: ["<", 2] }); diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js index 36361e531df..f7c605fdd2e 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js @@ -31,6 +31,7 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { fieldname: "status", label: __("Status"), fieldtype: "MultiSelectList", + options: ["Open", "Converted", "Quotation", "Replied"], get_data: function () { return [ { value: "Open", description: "Status" }, diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js index 2196500cfb0..b4b5955891e 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js @@ -56,6 +56,7 @@ frappe.query_reports["Job Card Summary"] = { label: __("Work Orders"), fieldname: "work_order", fieldtype: "MultiSelectList", + options: "Work Order", get_data: function (txt) { return frappe.db.get_link_options("Work Order", txt); }, @@ -64,6 +65,7 @@ frappe.query_reports["Job Card Summary"] = { label: __("Production Item"), fieldname: "production_item", fieldtype: "MultiSelectList", + options: "Item", get_data: function (txt) { return frappe.db.get_link_options("Item", txt); }, diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.js b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js index d6ca248abb5..40d87b8b0a2 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.js +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js @@ -41,7 +41,7 @@ frappe.query_reports["Production Planning Report"] = { fieldname: "docnames", label: __("Document Name"), fieldtype: "MultiSelectList", - options: "Sales Order", + options: "based_on", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js index f48f8b7ea18..74fbf47bb23 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js @@ -42,6 +42,7 @@ frappe.query_reports["Work Order Summary"] = { label: __("Sales Orders"), fieldname: "sales_order", fieldtype: "MultiSelectList", + options: "Sales Order", get_data: function (txt) { return frappe.db.get_link_options("Sales Order", txt); }, @@ -50,6 +51,7 @@ frappe.query_reports["Work Order Summary"] = { label: __("Production Item"), fieldname: "production_item", fieldtype: "MultiSelectList", + options: "Item", get_data: function (txt) { return frappe.db.get_link_options("Item", txt); }, diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 99ef5e40f3b..e4e9ec07b3f 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -87,6 +87,7 @@ function get_filters() { fieldname: "status", label: __("Status"), fieldtype: "MultiSelectList", + options: ["Overdue", "Unpaid", "Completed", "Partly Paid"], width: 100, get_data: function (txt) { let status = ["Overdue", "Unpaid", "Completed", "Partly Paid"]; diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js index e47e5f8ae4d..5866fcbc845 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js @@ -53,6 +53,7 @@ frappe.query_reports["Sales Order Analysis"] = { fieldname: "status", label: __("Status"), fieldtype: "MultiSelectList", + options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"], width: "80", get_data: function (txt) { let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"]; diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.js b/erpnext/stock/report/item_shortage_report/item_shortage_report.js index d293b895e5b..8ce71ddadcb 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.js +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.js @@ -16,6 +16,7 @@ frappe.query_reports["Item Shortage Report"] = { fieldname: "warehouse", label: __("Warehouse"), fieldtype: "MultiSelectList", + options: "Warehouse", width: "100", get_data: function (txt) { return frappe.db.get_link_options("Warehouse", txt); diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js index 3b66cb036a7..ab1e2babc80 100644 --- a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js @@ -50,6 +50,7 @@ frappe.query_reports["Serial and Batch Summary"] = { fieldname: "voucher_no", label: __("Voucher No"), fieldtype: "MultiSelectList", + options: "voucher_type", get_data: function (txt) { if (!frappe.query_report.filters) return; From dbe14d6fe4eb9d00e7a505db04f49d22722050af Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 13 Feb 2025 14:23:46 +0530 Subject: [PATCH 05/58] chore: resolve conflicts --- .../customer_ledger_summary.js | 25 ------------------- .../supplier_ledger_summary.js | 25 ------------------- 2 files changed, 50 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js index 771133df063..3600db852f8 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js @@ -92,30 +92,5 @@ frappe.query_reports["Customer Ledger Summary"] = { fieldtype: "Data", hidden: 1, }, -<<<<<<< HEAD -======= - { - fieldname: "cost_center", - label: __("Cost Center"), - fieldtype: "MultiSelectList", - options: "Cost Center", - get_data: function (txt) { - return frappe.db.get_link_options("Cost Center", txt, { - company: frappe.query_report.get_filter_value("company"), - }); - }, - }, - { - fieldname: "project", - label: __("Project"), - fieldtype: "MultiSelectList", - options: "Project", - get_data: function (txt) { - return frappe.db.get_link_options("Project", txt, { - company: frappe.query_report.get_filter_value("company"), - }); - }, - }, ->>>>>>> 8785342fce (fix(report): add options to multiselectlist fields) ], }; diff --git a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js index 7d031f88ecc..5d91575b8b2 100644 --- a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js +++ b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js @@ -74,30 +74,5 @@ frappe.query_reports["Supplier Ledger Summary"] = { fieldtype: "Data", hidden: 1, }, -<<<<<<< HEAD -======= - { - fieldname: "cost_center", - label: __("Cost Center"), - fieldtype: "MultiSelectList", - options: "Cost Center", - get_data: function (txt) { - return frappe.db.get_link_options("Cost Center", txt, { - company: frappe.query_report.get_filter_value("company"), - }); - }, - }, - { - fieldname: "project", - label: __("Project"), - fieldtype: "MultiSelectList", - options: "Project", - get_data: function (txt) { - return frappe.db.get_link_options("Project", txt, { - company: frappe.query_report.get_filter_value("company"), - }); - }, - }, ->>>>>>> 8785342fce (fix(report): add options to multiselectlist fields) ], }; From 1fb5586f569015fc41aca80eb591c8996fedb947 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 12 Feb 2025 16:09:22 +0530 Subject: [PATCH 06/58] fix: stock reservation for sales invoice --- erpnext/controllers/selling_controller.py | 7 +- .../doctype/sales_order/test_sales_order.py | 90 ++++--------------- 2 files changed, 20 insertions(+), 77 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 4b5b28c05fa..fec704edf64 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -794,6 +794,9 @@ class SellingController(StockController): def update_stock_reservation_entries(self) -> None: """Updates Delivered Qty in Stock Reservation Entries.""" + if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"): + return + # Don't update Delivered Qty on Return. if self.is_return: return @@ -831,7 +834,7 @@ class SellingController(StockController): sre_doc = frappe.get_doc("Stock Reservation Entry", sre) qty_can_be_deliver = 0 - if sre_doc.reservation_based_on == "Serial and Batch": + if sre_doc.reservation_based_on == "Serial and Batch" and item.serial_and_batch_bundle: sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) if sre_doc.has_serial_no: delivered_serial_nos = [d.serial_no for d in sbb.entries] @@ -900,7 +903,7 @@ class SellingController(StockController): sre_doc = frappe.get_doc("Stock Reservation Entry", sre) qty_can_be_undelivered = 0 - if sre_doc.reservation_based_on == "Serial and Batch": + if sre_doc.reservation_based_on == "Serial and Batch" and item.serial_and_batch_bundle: sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) if sre_doc.has_serial_no: serial_nos_to_undelivered = [d.serial_no for d in sbb.entries] diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 72cba2a1e9d..66fb7c55df8 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2120,99 +2120,39 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertRaises(frappe.ValidationError, so1.update_status, "Draft") def test_warehouse_mapping_based_on_stock_reservation(self): - frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", True) - self.create_company(company_name="Glass Ceiling", abbr="GC") - self.create_item("Lamy Safari 2", True, self.warehouse_stores, self.company) - self.create_customer() - self.clear_old_entries() - - so = frappe.new_doc("Sales Order") - so.company = self.company - so.customer = self.customer - so.transaction_date = today() - so.append( - "items", - { - "item_code": self.item, - "qty": 10, - "rate": 2000, - "warehouse": self.warehouse_stores, - "delivery_date": today(), - }, - ) - so.submit() - - # Create stock - se = frappe.get_doc( - { - "doctype": "Stock Entry", - "company": self.company, - "stock_entry_type": "Material Receipt", - "posting_date": today(), - "items": [ - {"item_code": self.item, "t_warehouse": self.warehouse_stores, "qty": 5}, - {"item_code": self.item, "t_warehouse": self.warehouse_finished_goods, "qty": 5}, - ], - } - ) - se.submit() - - # Reserve stock on 2 different warehouses - itm = so.items[0] - so.create_stock_reservation_entries( - [ - { - "sales_order_item": itm.name, - "item_code": itm.item_code, - "warehouse": self.warehouse_stores, - "qty_to_reserve": 2, - } - ] - ) - so.create_stock_reservation_entries( - [ - { - "sales_order_item": itm.name, - "item_code": itm.item_code, - "warehouse": self.warehouse_finished_goods, - "qty_to_reserve": 3, - } - ] - ) - - # Delivery note should auto-select warehouse based on reservation - dn = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) - self.assertEqual(2, len(dn.items)) - self.assertEqual(dn.items[0].qty, 2) - self.assertEqual(dn.items[0].warehouse, self.warehouse_stores) - self.assertEqual(dn.items[1].qty, 3) - + from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - warehouse = create_warehouse("Test Warehouse 1", company=self.company) + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", True) + company = "Glass Ceiling" + self.create_company(company_name=company, abbr="GC") + warehouse = create_warehouse("Test Reserved Warehouse", company=company) + create_item("Lamy Safari 2", is_stock_item=1, stock_uom="Nos", company=company) make_stock_entry( - item_code=self.item, + item_code="Lamy Safari 2", target=warehouse, - qty=5, - rate=200, - company=self.company, + qty=10, + rate=100, + company=company, + uom="Nos", ) so = frappe.new_doc("Sales Order") so.reserve_stock = 1 - so.company = self.company - so.customer = self.customer + so.company = company + so.customer = "_Test Customer" so.transaction_date = today() so.currency = "INR" so.append( "items", { - "item_code": self.item, + "item_code": "Lamy Safari 2", "qty": 5, "rate": 2000, "warehouse": warehouse, "delivery_date": today(), + "uom": "Nos", }, ) so.submit() From 52761affe2b29fb06b667e6b653ef5f8ea7ba5d8 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 13 Feb 2025 16:32:56 +0530 Subject: [PATCH 07/58] chore: fix test case --- .../doctype/sales_order/test_sales_order.py | 54 ------------------- 1 file changed, 54 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 66fb7c55df8..47d42b0a9d5 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2119,60 +2119,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertRaises(frappe.ValidationError, so1.update_status, "Draft") - def test_warehouse_mapping_based_on_stock_reservation(self): - from erpnext.stock.doctype.item.test_item import create_item - from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - - frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", True) - company = "Glass Ceiling" - self.create_company(company_name=company, abbr="GC") - warehouse = create_warehouse("Test Reserved Warehouse", company=company) - create_item("Lamy Safari 2", is_stock_item=1, stock_uom="Nos", company=company) - - make_stock_entry( - item_code="Lamy Safari 2", - target=warehouse, - qty=10, - rate=100, - company=company, - uom="Nos", - ) - - so = frappe.new_doc("Sales Order") - so.reserve_stock = 1 - so.company = company - so.customer = "_Test Customer" - so.transaction_date = today() - so.currency = "INR" - so.append( - "items", - { - "item_code": "Lamy Safari 2", - "qty": 5, - "rate": 2000, - "warehouse": warehouse, - "delivery_date": today(), - "uom": "Nos", - }, - ) - so.submit() - - sres = frappe.get_all( - "Stock Reservation Entry", - filters={"voucher_no": so.name}, - fields=["name"], - ) - - self.assertEqual(len(sres), 1) - sre_doc = frappe.get_doc("Stock Reservation Entry", sres[0].name) - self.assertFalse(sre_doc.status == "Delivered") - - si = make_sales_invoice(so.name) - si.update_stock = 1 - si.submit() - sre_doc.reload() - self.assertTrue(sre_doc.status == "Delivered") - def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") From abe5384449ac151a336f901ac5367e27aa6f484a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 13 Feb 2025 19:10:21 +0530 Subject: [PATCH 08/58] fix: allow scrap item with zero qty (cherry picked from commit 706cb64279a44a613c0fb7e1feacaac5bee03810) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 95d6d7079b7..65db490e7b5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2417,7 +2417,7 @@ class StockEntry(StockController): item_row = item_dict[d] child_qty = flt(item_row["qty"], precision) - if not self.is_return and child_qty <= 0: + if not self.is_return and child_qty <= 0 and not item_row.get("is_scrap_item"): continue se_child = self.append("items") From 18f94765f70a99535a9832784c3f0fb47bc44c16 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Fri, 14 Feb 2025 13:14:27 +0530 Subject: [PATCH 09/58] fix: include missing payment_gateway parameter in Payment Request URL (cherry picked from commit dbac8cfc948a90c00379832d9b9888406fdf4bc6) --- erpnext/accounts/doctype/payment_request/payment_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index f84f094e464..736bd548ff9 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -313,6 +313,7 @@ class PaymentRequest(Document): "payer_name": data.customer_name, "order_id": self.name, "currency": self.currency, + "payment_gateway": self.payment_gateway, } ) From e271a5cba0a969bd54fc57c3e384b7f3c226cecb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:34:37 +0530 Subject: [PATCH 10/58] feat: disable auto setting grand total to default mode of payment (backport #45591) (#45917) feat: disable auto setting grand total to default mode of payment (#45591) (cherry picked from commit f0a639905688125c10d5568e854174b69d36b59f) Co-authored-by: Diptanil Saha --- .../accounts/doctype/pos_profile/pos_profile.json | 9 ++++++++- erpnext/accounts/doctype/pos_profile/pos_profile.py | 1 + erpnext/public/js/controllers/taxes_and_totals.js | 8 +++++++- erpnext/selling/page/point_of_sale/pos_payment.js | 12 ++++++++++-- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 22f2965b86e..c38f9563eb2 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -29,6 +29,7 @@ "ignore_pricing_rule", "allow_rate_change", "allow_discount_change", + "disable_grand_total_to_default_mop", "section_break_23", "item_groups", "column_break_25", @@ -382,6 +383,12 @@ "fieldname": "print_receipt_on_order_complete", "fieldtype": "Check", "label": "Print Receipt on Order Complete" + }, + { + "default": "0", + "fieldname": "disable_grand_total_to_default_mop", + "fieldtype": "Check", + "label": "Disable auto setting Grand Total to default Payment Mode" } ], "icon": "icon-cog", @@ -409,7 +416,7 @@ "link_fieldname": "pos_profile" } ], - "modified": "2025-01-01 11:07:03.161950", + "modified": "2025-01-29 13:12:30.796630", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index ea27116e91c..cb9ae67fffd 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -36,6 +36,7 @@ class POSProfile(Document): currency: DF.Link customer: DF.Link | None customer_groups: DF.Table[POSCustomerGroup] + disable_grand_total_to_default_mop: DF.Check disable_rounded_total: DF.Check disabled: DF.Check expense_account: DF.Link | None diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 875e6980e1e..30ebb3d97ce 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -894,10 +894,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.frm.refresh_fields(); } - set_default_payment(total_amount_to_pay, update_paid_amount) { + async set_default_payment(total_amount_to_pay, update_paid_amount) { var me = this; var payment_status = true; if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { + let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop"); + + if (r.message.disable_grand_total_to_default_mop) { + return; + } + $.each(this.frm.doc['payments'] || [], function(index, data) { if(data.default && payment_status && total_amount_to_pay > 0) { let base_amount, amount; diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 33dd0489ba2..fc8b75031c8 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -340,11 +340,19 @@ erpnext.PointOfSale.Payment = class { // pass } - render_payment_section() { + async render_payment_section() { this.render_payment_mode_dom(); this.make_invoice_fields_control(); this.update_totals_section(); - this.focus_on_default_mop(); + let r = await frappe.db.get_value( + "POS Profile", + this.frm.doc.pos_profile, + "disable_grand_total_to_default_mop" + ); + + if (!r.message.disable_grand_total_to_default_mop) { + this.focus_on_default_mop(); + } } after_render() { From c289fef3b5f5945506a0b40fe722e518b2836e7a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 17 Feb 2025 13:17:11 +0530 Subject: [PATCH 11/58] fix: letter head for quality inspection (cherry picked from commit cdd41373b6920f413835f10b27c7bc3a4a2055ad) --- .../quality_inspection.json | 20 +++++++++++++++++-- .../quality_inspection/quality_inspection.py | 1 + 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index ebedd5b864b..9b440eabcfc 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -34,7 +34,9 @@ "verified_by", "column_break_17", "remarks", - "amended_from" + "amended_from", + "print_settings_section", + "letter_head" ], "fields": [ { @@ -255,6 +257,20 @@ "fieldtype": "Link", "label": "Company", "options": "Company" + }, + { + "fieldname": "print_settings_section", + "fieldtype": "Section Break", + "label": "Print Settings" + }, + { + "allow_on_submit": 1, + "fetch_from": "company.default_letter_head", + "fetch_if_empty": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head" } ], "icon": "fa fa-search", @@ -262,7 +278,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-01-16 17:00:48.774532", + "modified": "2025-02-17 13:20:17.583094", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 1eb565b5338..714db645ca6 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -37,6 +37,7 @@ class QualityInspection(Document): item_code: DF.Link item_name: DF.Data | None item_serial_no: DF.Link | None + letter_head: DF.Link | None manual_inspection: DF.Check naming_series: DF.Literal["MAT-QA-.YYYY.-"] quality_inspection_template: DF.Link | None From e998f063a99a4ef8b3f2005a246cc50101fb8e6a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:13:34 +0530 Subject: [PATCH 12/58] fix: pos accounting dimension fieldname error (backport #45899) (#45921) * fix: pos accounting dimension fieldname error (#45899) * fix: pos accounting dimension fieldname error * fix: method to get enabled accounting dimensions * fix: fetch enabled accounting dimensions * fix: clear flags for accounting_dimensions_details on_update * refactor: validation for doctype * fix: using get_checks_for_pl_and_bs_accounts for accounting dimensions (cherry picked from commit 60a5f4f30de2e3b9f6951ae370ab25e1609fe3e0) # Conflicts: # erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py * chore: resolve conflict * chore: resolve linter issue * fix: resolve linter issue * chore: resolve linter issue * chore: resolve linter issue * chore: resolve linter issue --------- Co-authored-by: Diptanil Saha --- .../accounting_dimension.py | 11 ++++--- .../pos_invoice_merge_log.py | 17 ++++++----- .../doctype/pos_profile/pos_profile.py | 29 +++++-------------- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index f8eeba84662..81937469f21 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -41,6 +41,11 @@ class AccountingDimension(Document): self.set_fieldname_and_label() def validate(self): + self.validate_doctype() + validate_column_name(self.fieldname) + self.validate_dimension_defaults() + + def validate_doctype(self): if self.document_type in ( *core_doctypes_list, "Accounting Dimension", @@ -62,9 +67,6 @@ class AccountingDimension(Document): if not self.is_new(): self.validate_document_type_change() - validate_column_name(self.fieldname) - self.validate_dimension_defaults() - def validate_document_type_change(self): doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type") if doctype_before_save != self.document_type: @@ -103,6 +105,7 @@ class AccountingDimension(Document): def on_update(self): frappe.flags.accounting_dimensions = None + frappe.flags.accounting_dimensions_details = None def make_dimension_in_accounting_doctypes(doc, doclist=None): @@ -263,7 +266,7 @@ def get_checks_for_pl_and_bs_accounts(): frappe.flags.accounting_dimensions_details = frappe.db.sql( """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c - WHERE p.name = c.parent""", + WHERE p.name = c.parent AND p.disabled = 0""", as_dict=1, ) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 18136a033b0..a8dd4c41c92 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -12,7 +12,9 @@ from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime from frappe.utils.background_jobs import enqueue, is_job_enqueued from frappe.utils.scheduler import is_scheduler_inactive -from erpnext.accounts.doctype.pos_profile.pos_profile import required_accounting_dimensions +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_checks_for_pl_and_bs_accounts, +) class POSInvoiceMergeLog(Document): @@ -292,22 +294,23 @@ class POSInvoiceMergeLog(Document): invoice.disable_rounded_total = cint( frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total") ) - accounting_dimensions = required_accounting_dimensions() + accounting_dimensions = get_checks_for_pl_and_bs_accounts() + accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions] dimension_values = frappe.db.get_value( - "POS Profile", {"name": invoice.pos_profile}, accounting_dimensions, as_dict=1 + "POS Profile", {"name": invoice.pos_profile}, accounting_dimensions_fields, as_dict=1 ) for dimension in accounting_dimensions: - dimension_value = dimension_values.get(dimension) + dimension_value = dimension_values.get(dimension.fieldname) - if not dimension_value: + if not dimension_value and (dimension.mandatory_for_pl or dimension.mandatory_for_bs): frappe.throw( _("Please set Accounting Dimension {} in {}").format( - frappe.bold(frappe.unscrub(dimension)), + frappe.bold(dimension.label), frappe.get_desk_link("POS Profile", invoice.pos_profile), ) ) - invoice.set(dimension, dimension_value) + invoice.set(dimension.fieldname, dimension_value) if self.merge_invoices_based_on == "Customer Group": invoice.flags.ignore_pos_profile = True diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index cb9ae67fffd..3b5aa5f75bd 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -7,6 +7,10 @@ from frappe import _, msgprint, scrub, unscrub from frappe.model.document import Document from frappe.utils import get_link_to_form, now +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_checks_for_pl_and_bs_accounts, +) + class POSProfile(Document): # begin: auto-generated types @@ -70,15 +74,15 @@ class POSProfile(Document): self.validate_accounting_dimensions() def validate_accounting_dimensions(self): - acc_dim_names = required_accounting_dimensions() - for acc_dim in acc_dim_names: - if not self.get(acc_dim): + acc_dims = get_checks_for_pl_and_bs_accounts() + for acc_dim in acc_dims: + if not self.get(acc_dim.fieldname) and (acc_dim.mandatory_for_pl or acc_dim.mandatory_for_bs): frappe.throw( _( "{0} is a mandatory Accounting Dimension.
" "Please set a value for {0} in Accounting Dimensions section." ).format( - unscrub(frappe.bold(acc_dim)), + frappe.bold(acc_dim.label), ), title=_("Mandatory Accounting Dimension"), ) @@ -216,23 +220,6 @@ def get_child_nodes(group_type, root): ) -def required_accounting_dimensions(): - p = frappe.qb.DocType("Accounting Dimension") - c = frappe.qb.DocType("Accounting Dimension Detail") - - acc_dim_doc = ( - frappe.qb.from_(p) - .inner_join(c) - .on(p.name == c.parent) - .select(c.parent) - .where((c.mandatory_for_bs == 1) | (c.mandatory_for_pl == 1)) - .where(p.disabled == 0) - ).run(as_dict=1) - - acc_dim_names = [scrub(d.parent) for d in acc_dim_doc] - return acc_dim_names - - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): From dd34bbe570af6fc5e6c57727ee111f4b51226a3e Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Mon, 17 Feb 2025 16:03:19 +0530 Subject: [PATCH 13/58] fix: pos return validation on v15 (#45951) --- erpnext/controllers/accounts_controller.py | 2 +- erpnext/controllers/sales_and_purchase_return.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e75fdc126e6..bb0a4070981 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -170,7 +170,7 @@ class AccountsController(TransactionBase): self.validate_qty_is_not_zero() if ( - self.doctype in ["Sales Invoice", "Purchase Invoice"] + self.doctype in ["Sales Invoice", "Purchase Invoice", "POS Invoice"] and self.get("is_return") and self.get("update_stock") ): diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 66ec8851727..a9007208a2a 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -258,7 +258,7 @@ def get_already_returned_items(doc): field = ( frappe.scrub(doc.doctype) + "_item" - if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice"] + if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice", "POS Invoice"] else "dn_detail" ) data = frappe.db.sql( @@ -770,6 +770,7 @@ def get_return_against_item_fields(voucher_type): "Delivery Note": "dn_detail", "Sales Invoice": "sales_invoice_item", "Subcontracting Receipt": "subcontracting_receipt_item", + "POS Invoice": "sales_invoice_item", } return return_against_item_fields[voucher_type] From 38edc46c46d9d3a46206f68115bebcee7c7218e5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:01:34 +0530 Subject: [PATCH 14/58] fix: disable partial payment in pos (backport #45752) (#45945) * fix: disable partial payment in pos (#45752) * fix: disable partial payment in pos * test: disable partial payment * test: removed print statement * test: using save method to auto calculate paid_amount * test: paid_amount calculation using save method * test: added save method to calculate paid_amount * test: outstanding amount * test: added test for partial payments in pos invoice * fix: custom validation error for partial payment * test: using partial payment validation * fix: validate only on submit (cherry picked from commit d94802067bc6b8ade91395df5de108a20c6ca979) # Conflicts: # erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py # erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py * chore: resolve conflict * chore: resolve conflict * chore: resolve linter issue * test: fixed failing test --------- Co-authored-by: Diptanil Saha --- .../test_pos_closing_entry.py | 28 ++++++++- .../doctype/pos_invoice/pos_invoice.py | 19 ++++++ .../doctype/pos_invoice/test_pos_invoice.py | 61 +++++++++++++++++-- .../test_pos_invoice_merge_log.py | 18 +++++- 4 files changed, 115 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index 5dd6259c585..34585ec200d 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -39,10 +39,12 @@ class TestPOSClosingEntry(unittest.TestCase): pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) @@ -68,6 +70,7 @@ class TestPOSClosingEntry(unittest.TestCase): pos_inv = create_pos_invoice(rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv.save() pos_inv.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) @@ -86,10 +89,12 @@ class TestPOSClosingEntry(unittest.TestCase): pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() # make return entry of pos_inv2 @@ -111,10 +116,12 @@ class TestPOSClosingEntry(unittest.TestCase): pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) @@ -165,6 +172,7 @@ class TestPOSClosingEntry(unittest.TestCase): opening_entry = create_opening_entry(pos_profile, test_user.name) pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() # if in between a mandatory accounting dimension is added to the POS Profile then @@ -218,11 +226,27 @@ class TestPOSClosingEntry(unittest.TestCase): opening_entry = create_opening_entry(pos_profile, test_user.name) pos_inv = create_pos_invoice( - item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no + item_code=item_code, + qty=5, + rate=300, + use_serial_batch_fields=1, + batch_no=batch_no, + do_not_submit=True, ) + pos_inv.payments[0].amount = pos_inv.grand_total + pos_inv.save() + pos_inv.submit() pos_inv2 = create_pos_invoice( - item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no + item_code=item_code, + qty=5, + rate=300, + use_serial_batch_fields=1, + batch_no=batch_no, + do_not_submit=True, ) + pos_inv2.payments[0].amount = pos_inv2.grand_total + pos_inv2.save() + pos_inv2.submit() batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code) self.assertEqual(batch_qty_with_pos, 0.0) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index ab5a4092c33..a8a733ac42c 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -20,6 +20,10 @@ from erpnext.controllers.queries import item_query as _item_query from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +class PartialPaymentValidationError(frappe.ValidationError): + pass + + class POSInvoice(SalesInvoice): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -210,6 +214,7 @@ class POSInvoice(SalesInvoice): self.validate_payment_amount() self.validate_loyalty_transaction() self.validate_company_with_pos_company() + self.validate_full_payment() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code @@ -477,6 +482,20 @@ class POSInvoice(SalesInvoice): if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points: validate_loyalty_points(self, self.loyalty_points) + def validate_full_payment(self): + invoice_total = flt(self.rounded_total) or flt(self.grand_total) + + if self.docstatus == 1: + if self.is_return and self.paid_amount != invoice_total: + frappe.throw( + msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError + ) + + if self.paid_amount < invoice_total: + frappe.throw( + msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError + ) + def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): if self.get("amended_from"): diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 7b6b8b50543..09c9443bdd9 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -7,7 +7,7 @@ import unittest import frappe from frappe import _ -from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return +from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.stock.doctype.item.test_item import make_item @@ -313,7 +313,7 @@ class TestPOSInvoice(unittest.TestCase): ) pos.append( - "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2000, "default": 1} ) pos.insert() @@ -324,6 +324,11 @@ class TestPOSInvoice(unittest.TestCase): # partial return 1 pos_return1.get("items")[0].qty = -1 + pos_return1.set("payments", []) + pos_return1.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1} + ) + pos_return1.paid_amount = -1000 pos_return1.submit() pos_return1.reload() @@ -338,6 +343,11 @@ class TestPOSInvoice(unittest.TestCase): # partial return 2 pos_return2 = make_sales_return(pos.name) + pos_return2.set("payments", []) + pos_return2.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1} + ) + pos_return2.paid_amount = -1000 pos_return2.submit() self.assertEqual(pos_return2.get("items")[0].qty, -1) @@ -373,6 +383,15 @@ class TestPOSInvoice(unittest.TestCase): inv.payments = [] self.assertRaises(frappe.ValidationError, inv.insert) + def test_partial_payment(self): + pos_inv = create_pos_invoice(rate=10000, do_not_save=1) + pos_inv.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 9000}, + ) + pos_inv.insert() + self.assertRaises(PartialPaymentValidationError, pos_inv.submit) + def test_serialized_item_transaction(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -581,7 +600,13 @@ class TestPOSInvoice(unittest.TestCase): "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" ) - inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000) + inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1) + inv.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000}, + ) + inv.insert() + inv.submit() lpe = frappe.get_doc( "Loyalty Point Entry", @@ -607,7 +632,13 @@ class TestPOSInvoice(unittest.TestCase): ) # add 10 loyalty points - create_pos_invoice(customer="Test Loyalty Customer", rate=10000) + pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1) + pos_inv.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000}, + ) + pos_inv.paid_amount = 10000 + pos_inv.submit() before_lp_details = get_loyalty_program_details_with_points( "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" @@ -641,10 +672,12 @@ class TestPOSInvoice(unittest.TestCase): test_user, pos_profile = init_user_and_profile() pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270}) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices() @@ -676,6 +709,7 @@ class TestPOSInvoice(unittest.TestCase): "included_in_print_rate": 1, }, ) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1) @@ -692,6 +726,7 @@ class TestPOSInvoice(unittest.TestCase): "included_in_print_rate": 1, }, ) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices() @@ -744,6 +779,7 @@ class TestPOSInvoice(unittest.TestCase): "included_in_print_rate": 1, }, ) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices() @@ -774,7 +810,10 @@ class TestPOSInvoice(unittest.TestCase): # POS Invoice 1, for the batch without bundle pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1) - + pos_inv1.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 4500}, + ) pos_inv1.items[0].batch_no = batch_no pos_inv1.save() pos_inv1.submit() @@ -790,8 +829,14 @@ class TestPOSInvoice(unittest.TestCase): # POS Invoice 2, for the batch with bundle pos_inv2 = create_pos_invoice( - item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no + item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no, do_not_save=1 ) + pos_inv2.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3000}, + ) + pos_inv2.save() + pos_inv2.submit() pos_inv2.reload() self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle) @@ -826,6 +871,10 @@ class TestPOSInvoice(unittest.TestCase): pos_inv1 = create_pos_invoice( item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01" ) + pos_inv1.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}, + ) pos_inv1.save() pos_inv1.submit() diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 904d8e83b9c..e0d37436be5 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -28,14 +28,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv = create_pos_invoice(rate=300, do_not_submit=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) + pos_inv3.save() pos_inv3.submit() consolidate_pos_invoices() @@ -61,14 +64,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv = create_pos_invoice(rate=300, do_not_submit=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) + pos_inv3.save() pos_inv3.submit() pos_inv_cn = make_sales_return(pos_inv.name) @@ -122,6 +128,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): }, ) inv.insert() + inv.payments[0].amount = inv.grand_total + inv.save() inv.submit() inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True) @@ -138,6 +146,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): }, ) inv2.insert() + inv2.payments[0].amount = inv.grand_total + inv2.save() inv2.submit() consolidate_pos_invoices() @@ -272,7 +282,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): inv2.submit() inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True) - inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000}) + inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1800}) inv3.insert() inv3.submit() @@ -280,8 +290,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): inv.load_from_db() consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) - self.assertEqual(consolidated_invoice.outstanding_amount, 800) - self.assertNotEqual(consolidated_invoice.status, "Paid") + self.assertNotEqual(consolidated_invoice.outstanding_amount, 800) + self.assertEqual(consolidated_invoice.status, "Paid") finally: frappe.set_user("Administrator") @@ -416,6 +426,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): do_not_submit=1, ) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv.save() pos_inv.submit() pos_inv_cn = make_sales_return(pos_inv.name) @@ -430,6 +441,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): do_not_submit=1, ) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices() From 8fbfe14c633826ea6a944214acf3368c6ff3cfe4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 17 Feb 2025 14:19:34 +0530 Subject: [PATCH 15/58] fix: provision to enable naming series for SABB (cherry picked from commit fe43975cdd6b57ca3dad21752238452170f2564e) # Conflicts: # erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json # erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py # erpnext/stock/doctype/stock_settings/stock_settings.json --- .../serial_and_batch_bundle.js | 7 ++ .../serial_and_batch_bundle.json | 13 ++++ .../serial_and_batch_bundle.py | 20 +++++ .../test_serial_and_batch_bundle.py | 74 +++++++++++++++++++ .../stock_settings/stock_settings.json | 27 +++++++ .../doctype/stock_settings/stock_settings.py | 2 + 6 files changed, 143 insertions(+) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js index ad8360ea75c..404abbd21bc 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js @@ -134,6 +134,13 @@ frappe.ui.form.on("Serial and Batch Bundle", { }, toggle_fields(frm) { + let show_naming_series_field = + frappe.user_defaults.set_serial_and_batch_bundle_naming_based_on_naming_series; + frm.toggle_display("naming_series", cint(show_naming_series_field)); + frm.toggle_reqd("naming_series", cint(show_naming_series_field)); + + frm.toggle_display("naming_series", frm.doc.__islocal ? true : false); + if (frm.doc.has_serial_no) { frm.doc.entries.forEach((row) => { if (Math.abs(row.qty) !== 1) { diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 3ff76ee818b..add0575ccc1 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "item_details_tab", + "naming_series", "company", "item_name", "has_serial_no", @@ -242,12 +243,24 @@ "fieldtype": "Data", "label": "Returned Against", "read_only": 1 + }, + { + "default": "SABB-.########", + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "\nSABB-.########", + "set_only_once": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-02-12 10:53:32.090309", +======= + "modified": "2025-02-17 16:22:36.056205", +>>>>>>> fe43975cdd (fix: provision to enable naming series for SABB) "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", 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 c37d2464ff0..c37051e1e2b 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 @@ -8,6 +8,7 @@ from collections import Counter, defaultdict import frappe from frappe import _, _dict, bold from frappe.model.document import Document +from frappe.model.naming import make_autoname from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import ( add_days, @@ -68,6 +69,7 @@ class SerialandBatchBundle(Document): item_code: DF.Link item_group: DF.Link | None item_name: DF.Data | None + naming_series: DF.Literal["", "SABB-.########"] posting_date: DF.Date | None posting_time: DF.Time | None returned_against: DF.Data | None @@ -80,6 +82,24 @@ class SerialandBatchBundle(Document): warehouse: DF.Link | None # end: auto-generated types + def autoname(self): + if frappe.db.get_single_value( + "Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series" + ): + if not self.naming_series: + frappe.throw(_("Naming Series is mandatory")) + + naming_series = self.naming_series + if "#" not in naming_series: + naming_series += ".#####" + + self.name = make_autoname(self.naming_series) + else: + try: + self.name = frappe.generate_hash(length=20) + except frappe.DuplicateEntryError: + self.autoname() + def validate(self): if self.docstatus == 1 and self.voucher_detail_no: self.validate_voucher_detail_no() diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 0a4352e1ea9..8748f8a785f 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -16,7 +16,81 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +<<<<<<< HEAD class TestSerialandBatchBundle(FrappeTestCase): +======= +class UnitTestSerialAndBatchBundle(UnitTestCase): + """ + Unit tests for SerialAndBatchBundle. + Use this class for testing individual functions and methods. + """ + + pass + + +class TestSerialandBatchBundle(IntegrationTestCase): + def test_naming_for_sabb(self): + frappe.db.set_single_value( + "Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series", 1 + ) + + serial_item_code = "New Serial No Valuation 11" + make_item( + serial_item_code, + { + "has_serial_no": 1, + "serial_no_series": "TEST-A-SER-VAL-.#####", + "is_stock_item": 1, + }, + ) + + for sn in ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"]: + if not frappe.db.exists("Serial No", sn): + frappe.get_doc( + { + "doctype": "Serial No", + "serial_no": sn, + "item_code": serial_item_code, + } + ).insert(ignore_permissions=True) + + bundle_doc = make_serial_batch_bundle( + { + "item_code": serial_item_code, + "warehouse": "_Test Warehouse - _TC", + "voucher_type": "Stock Entry", + "posting_date": today(), + "posting_time": nowtime(), + "qty": 10, + "serial_nos": ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"], + "type_of_transaction": "Inward", + "do_not_submit": True, + } + ) + + self.assertTrue(bundle_doc.name.startswith("SABB-")) + + frappe.db.set_single_value( + "Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series", 0 + ) + + bundle_doc = make_serial_batch_bundle( + { + "item_code": serial_item_code, + "warehouse": "_Test Warehouse - _TC", + "voucher_type": "Stock Entry", + "posting_date": today(), + "posting_time": nowtime(), + "qty": 10, + "serial_nos": ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"], + "type_of_transaction": "Inward", + "do_not_submit": True, + } + ) + + self.assertFalse(bundle_doc.name.startswith("SABB-")) + +>>>>>>> fe43975cdd (fix: provision to enable naming series for SABB) def test_inward_outward_serial_valuation(self): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index e542a1582e3..4b6d4f180e0 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -56,6 +56,8 @@ "use_serial_batch_fields", "do_not_update_serial_batch_on_creation_of_auto_bundle", "allow_existing_serial_no", + "serial_and_batch_bundle_section", + "set_serial_and_batch_bundle_naming_based_on_naming_series", "stock_planning_tab", "auto_material_request", "auto_indent", @@ -467,6 +469,27 @@ "fieldname": "allow_existing_serial_no", "fieldtype": "Check", "label": "Allow existing Serial No to be Manufactured/Received again" +<<<<<<< HEAD +======= + }, + { + "default": "0", + "description": "Upon submission of the Sales Order, Work Order, or Production Plan, the system will automatically reserve the stock.", + "fieldname": "auto_reserve_stock", + "fieldtype": "Check", + "label": "Auto Reserve Stock" + }, + { + "fieldname": "serial_and_batch_bundle_section", + "fieldtype": "Section Break", + "label": "Serial and Batch Bundle" + }, + { + "default": "0", + "fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series", + "fieldtype": "Check", + "label": "Set Serial and Batch Bundle Naming Based on Naming Series" +>>>>>>> fe43975cdd (fix: provision to enable naming series for SABB) } ], "icon": "icon-cog", @@ -474,7 +497,11 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2024-12-09 17:52:36.030456", +======= + "modified": "2025-02-17 13:36:36.177743", +>>>>>>> fe43975cdd (fix: provision to enable naming series for SABB) "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index b7a317cd66a..8589004c8bf 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -55,6 +55,7 @@ class StockSettings(Document): role_allowed_to_create_edit_back_dated_transactions: DF.Link | None role_allowed_to_over_deliver_receive: DF.Link | None sample_retention_warehouse: DF.Link | None + set_serial_and_batch_bundle_naming_based_on_naming_series: DF.Check show_barcode_field: DF.Check stock_auth_role: DF.Link | None stock_frozen_upto: DF.Date | None @@ -75,6 +76,7 @@ class StockSettings(Document): "default_warehouse", "set_qty_in_transactions_based_on_serial_no_input", "use_serial_batch_fields", + "set_serial_and_batch_bundle_naming_based_on_naming_series", ]: frappe.db.set_default(key, self.get(key, "")) From 2f7f9c0bac8aa69c163611907e4f8100a17fd3a4 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 17 Feb 2025 21:29:22 +0530 Subject: [PATCH 16/58] chore: fix conflicts --- .../serial_and_batch_bundle/serial_and_batch_bundle.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index add0575ccc1..02ecc11f44a 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -256,11 +256,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2025-02-12 10:53:32.090309", -======= - "modified": "2025-02-17 16:22:36.056205", ->>>>>>> fe43975cdd (fix: provision to enable naming series for SABB) + "modified": "2025-02-17 18:22:36.056205", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", From 697fdf5bc35853dc7c173de47dbc036e1d6efa35 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 17 Feb 2025 21:30:15 +0530 Subject: [PATCH 17/58] chore: fix conflicts --- .../doctype/stock_settings/stock_settings.json | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 4b6d4f180e0..22a24d1bfa1 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -469,15 +469,6 @@ "fieldname": "allow_existing_serial_no", "fieldtype": "Check", "label": "Allow existing Serial No to be Manufactured/Received again" -<<<<<<< HEAD -======= - }, - { - "default": "0", - "description": "Upon submission of the Sales Order, Work Order, or Production Plan, the system will automatically reserve the stock.", - "fieldname": "auto_reserve_stock", - "fieldtype": "Check", - "label": "Auto Reserve Stock" }, { "fieldname": "serial_and_batch_bundle_section", @@ -489,7 +480,6 @@ "fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series", "fieldtype": "Check", "label": "Set Serial and Batch Bundle Naming Based on Naming Series" ->>>>>>> fe43975cdd (fix: provision to enable naming series for SABB) } ], "icon": "icon-cog", @@ -497,11 +487,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], -<<<<<<< HEAD - "modified": "2024-12-09 17:52:36.030456", -======= - "modified": "2025-02-17 13:36:36.177743", ->>>>>>> fe43975cdd (fix: provision to enable naming series for SABB) + "modified": "2025-02-17 14:36:36.177743", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From 43d32eb10ec7b475dd8edbe667de489be986d069 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 17 Feb 2025 21:32:48 +0530 Subject: [PATCH 18/58] chore: fix conflicts --- .../test_serial_and_batch_bundle.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 8748f8a785f..a3bd20b80ba 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -16,19 +16,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -<<<<<<< HEAD class TestSerialandBatchBundle(FrappeTestCase): -======= -class UnitTestSerialAndBatchBundle(UnitTestCase): - """ - Unit tests for SerialAndBatchBundle. - Use this class for testing individual functions and methods. - """ - - pass - - -class TestSerialandBatchBundle(IntegrationTestCase): def test_naming_for_sabb(self): frappe.db.set_single_value( "Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series", 1 @@ -90,7 +78,6 @@ class TestSerialandBatchBundle(IntegrationTestCase): self.assertFalse(bundle_doc.name.startswith("SABB-")) ->>>>>>> fe43975cdd (fix: provision to enable naming series for SABB) def test_inward_outward_serial_valuation(self): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt From bb3eb8117043ceea1558ea85349511d23bb704b1 Mon Sep 17 00:00:00 2001 From: rethik Date: Mon, 3 Feb 2025 19:22:37 +0530 Subject: [PATCH 19/58] fix: add validate to allow equity account and party_type shareholder (cherry picked from commit 2c8e3f3409bd755ebd0ce7faa22f7aa1ac148d7a) --- erpnext/accounts/party.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 0df9dcb0683..38c69ca7eb9 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -765,7 +765,11 @@ def validate_account_party_type(self): if self.party_type and self.party: account_type = frappe.get_cached_value("Account", self.account, "account_type") - if account_type and (account_type not in ["Receivable", "Payable"]): + if ( + account_type + and (account_type not in ["Receivable", "Payable", "Equity"]) + and self.party_type != "Shareholder" + ): frappe.throw( _( "Party Type and Party can only be set for Receivable / Payable account

" "{0}" From 552b5a79ce338bd518e1f10d841c99d102002612 Mon Sep 17 00:00:00 2001 From: rethik Date: Mon, 3 Feb 2025 19:29:59 +0530 Subject: [PATCH 20/58] test: add unit test to validate account type and party type (cherry picked from commit 9422ce5aee303c738de4fda9dc01cc02fd1bbfc9) --- .../accounts/doctype/gl_entry/test_gl_entry.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py index f6ed163bff5..6ff378559c0 100644 --- a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py @@ -124,3 +124,20 @@ class TestGLEntry(unittest.TestCase): str(e), "Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC", ) + + def test_validate_account_party_type_shareholder(self): + jv = make_journal_entry( + "Opening Balance Equity - _TC", + "Cash - _TC", + 100, + "_Test Cost Center - _TC", + save=False, + submit=False, + ) + + for row in jv.accounts: + row.party_type = "Shareholder" + break + + jv.save().submit() + self.assertEqual(1, jv.docstatus) From 0d2115197e5345b532cdee80dfcb0029acea348e Mon Sep 17 00:00:00 2001 From: rethik Date: Tue, 4 Feb 2025 11:07:41 +0530 Subject: [PATCH 21/58] fix: remove party type from validate (cherry picked from commit f82837a4a28d387a4e80ec0df9729ddfa104240f) --- erpnext/accounts/party.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 38c69ca7eb9..f2e86ef2b2e 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -765,11 +765,7 @@ def validate_account_party_type(self): if self.party_type and self.party: account_type = frappe.get_cached_value("Account", self.account, "account_type") - if ( - account_type - and (account_type not in ["Receivable", "Payable", "Equity"]) - and self.party_type != "Shareholder" - ): + if account_type and (account_type not in ["Receivable", "Payable", "Equity"]): frappe.throw( _( "Party Type and Party can only be set for Receivable / Payable account

" "{0}" From c8881a93584f0969134da170b57bd0e9d32eaa70 Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Wed, 5 Feb 2025 11:00:55 +0530 Subject: [PATCH 22/58] fix: validate payment request total of partly paid invoice (cherry picked from commit 899c18df180cea20a0fb88e730ba047b1d256ddd) --- erpnext/accounts/doctype/payment_request/payment_request.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 736bd548ff9..84e44621784 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -775,7 +775,10 @@ def get_existing_paid_amount(doctype, name): frappe.qb.from_(PL) .left_join(PER) .on( - (PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no) + (PL.against_voucher_type == PER.reference_doctype) + & (PL.against_voucher_no == PER.reference_name) + & (PL.voucher_type == PER.parenttype) + & (PL.voucher_no == PER.parent) ) .select(Abs(Sum(PL.amount)).as_("total_paid_amount")) .where(PL.against_voucher_type.eq(doctype)) From 9a33b877f5a92f5152e7a5176fce5db17d092ad3 Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Thu, 6 Feb 2025 11:59:29 +0530 Subject: [PATCH 23/58] test: add unit test to validate payment request grand_total for partly paid invoice (cherry picked from commit f8472c32d9f5a051ac676f512eef43defeb81988) --- .../payment_request/test_payment_request.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index ed940470d6c..7ce6cc0b7b7 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -542,6 +542,45 @@ class TestPaymentRequest(FrappeTestCase): self.assertEqual(pr.grand_total, si.outstanding_amount) + def test_partial_paid_invoice_with_more_payment_entry(self): + pi = make_purchase_invoice(currency="INR", qty=1, rate=500) + pi.submit() + pi_1 = make_purchase_invoice(currency="INR", qty=1, rate=300) + pi_1.submit() + + pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1) + pr.grand_total = 200 + pr.submit() + pr.create_payment_entry() + pr_1 = make_payment_request( + dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1 + ) + pr_1.grand_total = 200 + pr_1.submit() + pr_1.create_payment_entry() + + pe = get_payment_entry(dt="Purchase Invoice", dn=pi.name) + pe.paid_amount = 200 + pe.references[0].reference_doctype = pi.doctype + pe.references[0].reference_name = pi.name + pe.references[0].grand_total = pi.grand_total + pe.references[0].outstanding_amount = pi.outstanding_amount + pe.references[0].allocated_amount = 100 + pe.append( + "references", + { + "reference_doctype": pi_1.doctype, + "reference_name": pi_1.name, + "grand_total": pi_1.grand_total, + "outstanding_amount": pi_1.outstanding_amount, + "allocated_amount": 100, + }, + ) + + pr_2 = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1) + pi.load_from_db() + self.assertEqual(pr_2.grand_total, pi.outstanding_amount) + def test_partial_paid_invoice_with_submitted_payment_entry(self): pi = make_purchase_invoice(currency="INR", qty=1, rate=5000) From 2b80c009b3ec7133736cfbfb0e99af295deac2ff Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 18 Feb 2025 12:19:06 +0530 Subject: [PATCH 24/58] fix: serial no status for internal transfer delivery note (cherry picked from commit 3333331a3dae4daaf88fb812d21df7c97dd30b51) --- erpnext/controllers/selling_controller.py | 7 ++-- .../delivery_note/test_delivery_note.py | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index fec704edf64..8ba8f06c65f 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -596,12 +596,13 @@ class SellingController(StockController): if not self.is_internal_transfer() or self.docstatus == 1 else None ) - if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return: - if self.docstatus == 1: + + if self.is_internal_transfer(): + if serial_and_batch_bundle and self.docstatus == 1 and self.is_return: serial_and_batch_bundle = self.make_package_for_transfer( serial_and_batch_bundle, item_row.warehouse, type_of_transaction="Inward" ) - else: + elif not serial_and_batch_bundle: serial_and_batch_bundle = frappe.db.get_value( "Stock Ledger Entry", {"voucher_detail_no": item_row.name, "warehouse": item_row.warehouse}, diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 4afaffe6fd4..f86798d17e8 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -769,6 +769,48 @@ class TestDeliveryNote(FrappeTestCase): {"warehouse": "_Test Warehouse - _TC"}, ) + def test_delivery_note_internal_transfer_serial_no_status(self): + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + + item = make_item( + "_Test Item for Internal Transfer With Serial No Status", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "INT-SN-.####"}, + ).name + + warehouse = "_Test Warehouse - _TC" + target = "Stores - _TC" + company = "_Test Company" + customer = create_internal_customer(represents_company=company) + rate = 42 + + se = make_stock_entry(target=warehouse, qty=5, basic_rate=rate, item_code=item) + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle) + + dn = create_delivery_note( + item_code=item, + company=company, + customer=customer, + qty=5, + rate=500, + warehouse=warehouse, + target_warehouse=target, + ignore_pricing_rule=0, + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos), + ) + + for serial_no in serial_nos: + sn = frappe.db.get_value("Serial No", serial_no, ["status", "warehouse"], as_dict=1) + self.assertEqual(sn.status, "Active") + self.assertEqual(sn.warehouse, target) + + dn.cancel() + + for serial_no in serial_nos: + sn = frappe.db.get_value("Serial No", serial_no, ["status", "warehouse"], as_dict=1) + self.assertEqual(sn.status, "Active") + self.assertEqual(sn.warehouse, warehouse) + def test_delivery_of_bundled_items_to_target_warehouse(self): from erpnext.selling.doctype.customer.test_customer import create_internal_customer From b32e4daf2b2a38e31b7e9213eb05d71217c94269 Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Thu, 6 Feb 2025 13:31:10 +0530 Subject: [PATCH 25/58] fix: add accounting dimensions section in sales order item (cherry picked from commit 7d47869f4b0b6a5b0b05cda14a1290daa9cdc60f) # Conflicts: # erpnext/selling/doctype/sales_order_item/sales_order_item.json --- .../sales_order_item/sales_order_item.json | 40 ++++++++++++++++++- .../sales_order_item/sales_order_item.py | 2 + 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index fb9e895ccb7..2df6dc3ea83 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -106,7 +106,11 @@ "purchase_order", "column_break_89", "material_request_item", - "purchase_order_item" + "purchase_order_item", + "accounting_dimensions_section", + "cost_center", + "column_break_ihdh", + "project" ], "fields": [ { @@ -926,12 +930,46 @@ "fieldname": "available_quantity_section", "fieldtype": "Section Break", "label": "Available Quantity" + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "allow_on_submit": 1, + "default": ":Company", + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center", + "print_hide": 1, + "print_width": "120px", + "reqd": 1, + "width": "120px" + }, + { + "fieldname": "column_break_ihdh", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project", + "search_index": 1 } ], "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2024-11-21 14:21:29.743474", +======= + "modified": "2025-02-06 13:29:24.619850", +>>>>>>> 7d47869f4b (fix: add accounting dimensions section in sales order item) "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py index 888ea755e2e..7f2a37b1616 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -32,6 +32,7 @@ class SalesOrderItem(Document): brand: DF.Link | None company_total_stock: DF.Float conversion_factor: DF.Float + cost_center: DF.Link customer_item_code: DF.Data | None delivered_by_supplier: DF.Check delivered_qty: DF.Float @@ -68,6 +69,7 @@ class SalesOrderItem(Document): pricing_rules: DF.SmallText | None produced_qty: DF.Float production_plan_qty: DF.Float + project: DF.Link | None projected_qty: DF.Float purchase_order: DF.Link | None purchase_order_item: DF.Data | None From 52860cc56639ebdd8143e869300838ad1b8854ff Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 18 Feb 2025 14:27:14 +0530 Subject: [PATCH 26/58] chore: resolve conflict --- .../selling/doctype/sales_order_item/sales_order_item.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 2df6dc3ea83..500a7657176 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -965,11 +965,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2024-11-21 14:21:29.743474", -======= "modified": "2025-02-06 13:29:24.619850", ->>>>>>> 7d47869f4b (fix: add accounting dimensions section in sales order item) "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", From 8306d6fdb6a9ab08d0458dd0c77673e372f85da3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 18 Feb 2025 13:52:26 +0530 Subject: [PATCH 27/58] fix: slow query (cherry picked from commit 8cfab57fc8d709ba3087294002b35880a10efc56) # Conflicts: # erpnext/stock/doctype/packed_item/packed_item.json --- erpnext/stock/doctype/packed_item/packed_item.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 1daf6791d40..8c49c2865d1 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -202,7 +202,8 @@ "oldfieldname": "parent_detail_docname", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "depends_on": "batch_no", @@ -295,7 +296,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2024-02-04 16:30:44.263964", +======= + "modified": "2025-02-18 13:06:02.789654", +>>>>>>> 8cfab57fc8 (fix: slow query) "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", From 567fb8abd1de19c1a36735e2e23e7c50a7c65129 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 18 Feb 2025 12:37:07 +0530 Subject: [PATCH 28/58] fix: set sco_qty field of PO to non negative (cherry picked from commit dfc3dc4944447d120c91568005289249c722e308) --- .../doctype/purchase_order_item/purchase_order_item.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 4fc20594ffa..1724e3cc99c 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -917,6 +917,7 @@ "fieldtype": "Float", "label": "Subcontracted Quantity", "no_copy": 1, + "non_negative": 1, "read_only": 1 } ], @@ -924,7 +925,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-12-10 12:11:18.536089", + "modified": "2025-02-18 12:35:04.432636", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", From 0acdae02c194aaa11df09213572c48a1424d1d85 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 18 Feb 2025 20:00:08 +0530 Subject: [PATCH 29/58] chore: fix conflicts --- erpnext/stock/doctype/packed_item/packed_item.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 8c49c2865d1..cb415e3813a 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -296,11 +296,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2024-02-04 16:30:44.263964", -======= - "modified": "2025-02-18 13:06:02.789654", ->>>>>>> 8cfab57fc8 (fix: slow query) + "modified": "2025-02-18 13:07:02.789654", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", @@ -310,4 +306,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From 42923656eea4e69a8a709495bb8579fad6b42db5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 18 Feb 2025 13:56:00 +0530 Subject: [PATCH 30/58] fix: millisecond issue for posting datetime (cherry picked from commit ac9e5c01633922628233cb851b57ed6fc006a7cc) # Conflicts: # erpnext/patches.txt # erpnext/stock/utils.py --- erpnext/patches.txt | 4 +++ .../patches/v14_0/update_posting_datetime.py | 10 +++++++ erpnext/stock/utils.py | 27 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 erpnext/patches/v14_0/update_posting_datetime.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index bb76f948f3a..319691aa93b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -392,4 +392,8 @@ erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect erpnext.patches.v15_0.sync_auto_reconcile_config execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment") erpnext.patches.v14_0.disable_add_row_in_gross_profit +<<<<<<< HEAD erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment +======= +erpnext.patches.v14_0.update_posting_datetime +>>>>>>> ac9e5c0163 (fix: millisecond issue for posting datetime) diff --git a/erpnext/patches/v14_0/update_posting_datetime.py b/erpnext/patches/v14_0/update_posting_datetime.py new file mode 100644 index 00000000000..cb28193b74b --- /dev/null +++ b/erpnext/patches/v14_0/update_posting_datetime.py @@ -0,0 +1,10 @@ +import frappe + + +def execute(): + frappe.db.sql( + """ + UPDATE `tabStock Ledger Entry` + SET posting_datetime = timestamp(posting_date, posting_time) + """ + ) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 6369562a62d..9ee836fe9b5 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -663,4 +663,31 @@ def get_combine_datetime(posting_date, posting_time): if isinstance(posting_time, datetime.timedelta): posting_time = (datetime.datetime.min + posting_time).time() +<<<<<<< HEAD return datetime.datetime.combine(posting_date, posting_time).replace(microsecond=0) +======= + return datetime.datetime.combine(posting_date, posting_time) + + +@frappe.request_cache +def get_default_stock_uom() -> str | None: + if default_uom := frappe.get_cached_value("Stock Settings", None, "stock_uom"): + return default_uom + + acceptable_default_uoms = dict.fromkeys( + ( + "Nos", + # In the past, we used to create translated UOMs during initial setup. + # These could either be in the system language... + _("Nos", frappe.get_system_settings("language")), + # or the current user's language + _("Nos"), + ) + ) + + available_default_uoms = frappe.db.get_values( + "UOM", {"name": ("in", tuple(acceptable_default_uoms))}, pluck="name" + ) + + return next((uom for uom in acceptable_default_uoms if uom in available_default_uoms), None) +>>>>>>> ac9e5c0163 (fix: millisecond issue for posting datetime) From 050bb1eef5f0688cb39e0aa1a6bea2a622ebc45d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 18 Feb 2025 20:10:30 +0530 Subject: [PATCH 31/58] chore: fix conflicts --- erpnext/stock/utils.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 9ee836fe9b5..f1055b114cf 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -663,31 +663,4 @@ def get_combine_datetime(posting_date, posting_time): if isinstance(posting_time, datetime.timedelta): posting_time = (datetime.datetime.min + posting_time).time() -<<<<<<< HEAD - return datetime.datetime.combine(posting_date, posting_time).replace(microsecond=0) -======= return datetime.datetime.combine(posting_date, posting_time) - - -@frappe.request_cache -def get_default_stock_uom() -> str | None: - if default_uom := frappe.get_cached_value("Stock Settings", None, "stock_uom"): - return default_uom - - acceptable_default_uoms = dict.fromkeys( - ( - "Nos", - # In the past, we used to create translated UOMs during initial setup. - # These could either be in the system language... - _("Nos", frappe.get_system_settings("language")), - # or the current user's language - _("Nos"), - ) - ) - - available_default_uoms = frappe.db.get_values( - "UOM", {"name": ("in", tuple(acceptable_default_uoms))}, pluck="name" - ) - - return next((uom for uom in acceptable_default_uoms if uom in available_default_uoms), None) ->>>>>>> ac9e5c0163 (fix: millisecond issue for posting datetime) From 2e3b19ebb26dc1ecf39cef5b21172b275821d507 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 18 Feb 2025 20:11:02 +0530 Subject: [PATCH 32/58] chore: fix conflicts --- erpnext/patches.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 319691aa93b..cb10fcce1c4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -392,8 +392,5 @@ erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect erpnext.patches.v15_0.sync_auto_reconcile_config execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment") erpnext.patches.v14_0.disable_add_row_in_gross_profit -<<<<<<< HEAD erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment -======= erpnext.patches.v14_0.update_posting_datetime ->>>>>>> ac9e5c0163 (fix: millisecond issue for posting datetime) From 22eaa141790814d28baecb7f54758cdaa07d3eb9 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 18 Feb 2025 14:22:39 +0530 Subject: [PATCH 33/58] fix: check if employee is currently working on another workstation (cherry picked from commit 8234e659c8e20ee60a96b63f1f50eafdac974420) --- .../manufacturing/doctype/job_card/job_card.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 1b4840d14b5..c82038af342 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -216,7 +216,7 @@ class JobCard(Document): open_job_cards = [] if d.get("employee"): - open_job_cards = self.get_open_job_cards(d.get("employee")) + open_job_cards = self.get_open_job_cards(d.get("employee"), workstation=self.workstation) data = self.get_overlap_for(d, open_job_cards=open_job_cards) if data: @@ -257,9 +257,12 @@ class JobCard(Document): frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1 ) - if args.get("employee"): - # override capacity for employee - production_capacity = 1 + if self.get_open_job_cards(args.get("employee")): + frappe.throw( + _( + "Employee {0} is currently working on another workstation. Please assign another employee." + ).format(args.get("employee")) + ) if not self.has_overlap(production_capacity, time_logs): return {} @@ -366,7 +369,7 @@ class JobCard(Document): return time_logs - def get_open_job_cards(self, employee): + def get_open_job_cards(self, employee, workstation=None): jc = frappe.qb.DocType("Job Card") jctl = frappe.qb.DocType("Job Card Time Log") @@ -377,13 +380,15 @@ class JobCard(Document): .select(jc.name) .where( (jctl.parent == jc.name) - & (jc.workstation == self.workstation) & (jctl.employee == employee) & (jc.docstatus < 1) & (jc.name != self.name) ) ) + if workstation: + query = query.where(jc.workstation == workstation) + jobs = query.run(as_dict=True) return [job.get("name") for job in jobs] if jobs else [] From 5bccf9f837e2d5a1e97a2f3d42afd74887a29ba8 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 18 Feb 2025 14:49:52 +0530 Subject: [PATCH 34/58] fix: throw correct exception (cherry picked from commit 4487edb255175349613963db572d3bf55c7322b5) --- erpnext/manufacturing/doctype/job_card/job_card.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index c82038af342..90ccff720e8 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -261,7 +261,8 @@ class JobCard(Document): frappe.throw( _( "Employee {0} is currently working on another workstation. Please assign another employee." - ).format(args.get("employee")) + ).format(args.get("employee")), + OverlapError, ) if not self.has_overlap(production_capacity, time_logs): From bd89c19c98fca7e90a22ccde96aa92cba2d29566 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Wed, 12 Feb 2025 12:54:30 +0530 Subject: [PATCH 35/58] fix(quotation): fetch exchange rate on currency change (cherry picked from commit 2f77a8bed11311c6b552fbbbca2fcc986aed98c1) --- .../selling/doctype/quotation/quotation.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 7311857e350..03665b48a85 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -353,6 +353,26 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. ); dialog.show(); } + + currency() { + super.currency(); + let me = this; + const company_currency = this.get_company_currency(); + if (this.frm.doc.currency && this.frm.doc.currency !== company_currency) { + this.get_exchange_rate( + this.frm.doc.transaction_date, + this.frm.doc.currency, + company_currency, + function (exchange_rate) { + if (exchange_rate != me.frm.doc.conversion_rate) { + me.set_margin_amount_based_on_currency(exchange_rate); + me.set_actual_charges_based_on_currency(exchange_rate); + me.frm.set_value("conversion_rate", exchange_rate); + } + } + ); + } + } }; cur_frm.script_manager.make(erpnext.selling.QuotationController); From 8fb9228871cdf27c5acb757297525da22cdf101b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 5 Feb 2025 18:48:52 +0530 Subject: [PATCH 36/58] feat: added option to enforce free item qty in pricing rule (cherry picked from commit 19c01b145765fbbd2be95e914c818c44697b7d76) --- .../accounts/doctype/pricing_rule/pricing_rule.json | 10 +++++++++- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 1 + erpnext/accounts/doctype/pricing_rule/utils.py | 3 ++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index ee9dd2be8c3..473e9b837ae 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -53,6 +53,7 @@ "column_break_42", "free_item_uom", "round_free_qty", + "enforce_free_item_qty", "is_recursive", "recurse_for", "apply_recursion_over", @@ -643,12 +644,19 @@ "fieldname": "has_priority", "fieldtype": "Check", "label": "Has Priority" + }, + { + "default": "0", + "depends_on": "eval:doc.price_or_product_discount == 'Product'", + "fieldname": "enforce_free_item_qty", + "fieldtype": "Check", + "label": "Enforce Free Item Qty" } ], "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2024-09-16 18:14:51.314765", + "modified": "2025-02-05 18:05:03.886828", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 73cb2483811..bc830ea0450 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -60,6 +60,7 @@ class PricingRule(Document): disable: DF.Check discount_amount: DF.Currency discount_percentage: DF.Float + enforce_free_item_qty: DF.Check for_price_list: DF.Link | None free_item: DF.Link | None free_item_rate: DF.Currency diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 551eaa3d1ce..adadcceb7e7 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -713,7 +713,8 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args): args.pop((item.item_code, item.pricing_rules)) for free_item in args.values(): - doc.append("items", free_item) + if frappe.get_value("Pricing Rule", free_item["pricing_rules"], "enforce_free_item_qty"): + doc.append("items", free_item) def get_pricing_rule_items(pr_doc, other_items=False) -> list: From f63a9dbf9b7b5141e945cd372e6bc10a6af45333 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 5 Feb 2025 19:18:01 +0530 Subject: [PATCH 37/58] fix: tests (cherry picked from commit 366ae85d855c130c89fa1f151a2cca5b1e7dbdd8) --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 2 +- erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py | 5 +++++ erpnext/stock/doctype/pick_list/test_pick_list.py | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index bc830ea0450..2d82144a50b 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -646,7 +646,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra if pricing_rule.margin_type in ["Percentage", "Amount"]: item_details.margin_rate_or_amount = 0.0 item_details.margin_type = None - elif pricing_rule.get("free_item"): + elif pricing_rule.get("free_item") and pricing_rule.get("enforce_free_item_qty"): item_details.remove_free_item = ( item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item") ) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 965e2b267a3..0bceb2ffa7f 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -386,6 +386,7 @@ class TestPricingRule(FrappeTestCase): "price_or_product_discount": "Product", "same_item": 1, "free_qty": 1, + "enforce_free_item_qty": 1, "company": "_Test Company", } frappe.get_doc(test_record.copy()).insert() @@ -418,6 +419,7 @@ class TestPricingRule(FrappeTestCase): "same_item": 0, "free_item": "_Test Item 2", "free_qty": 1, + "enforce_free_item_qty": 1, "company": "_Test Company", } frappe.get_doc(test_record.copy()).insert() @@ -1111,6 +1113,7 @@ class TestPricingRule(FrappeTestCase): "price_or_product_discount": "Product", "same_item": 1, "free_qty": 1, + "enforce_free_item_qty": 1, "round_free_qty": 1, "is_recursive": 1, "recurse_for": 2, @@ -1156,6 +1159,7 @@ class TestPricingRule(FrappeTestCase): "price_or_product_discount": "Product", "same_item": 1, "free_qty": 10, + "enforce_free_item_qty": 1, "round_free_qty": 1, "is_recursive": 1, "recurse_for": 100, @@ -1451,6 +1455,7 @@ def make_pricing_rule(**args): "discount_amount": args.discount_amount or 0.0, "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0, "has_priority": args.has_priority or 0, + "enforce_free_item_qty": args.enforce_free_item_qty or 1, } ) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 624b169b5ae..0f655460f41 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -1242,6 +1242,7 @@ class TestPickList(FrappeTestCase): "is_recursive": 1, "recurse_for": 2, "free_qty": 1, + "enforce_free_item_qty": 1, "company": "_Test Company", "customer": "_Test Customer", } From ad1120810921490d7abff2ef1e9a498bf48f3ef7 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 5 Feb 2025 21:34:19 +0530 Subject: [PATCH 38/58] test: added test (cherry picked from commit ac3259b8f156131e2d5ab27e411f9d735f835626) --- .../doctype/pricing_rule/pricing_rule.json | 4 +- .../doctype/pricing_rule/test_pricing_rule.py | 52 +++++++++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 473e9b837ae..3b743c796af 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -646,7 +646,7 @@ "label": "Has Priority" }, { - "default": "0", + "default": "1", "depends_on": "eval:doc.price_or_product_discount == 'Product'", "fieldname": "enforce_free_item_qty", "fieldtype": "Check", @@ -656,7 +656,7 @@ "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2025-02-05 18:05:03.886828", + "modified": "2025-02-05 21:03:22.103044", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 0bceb2ffa7f..75fb2632b17 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -386,7 +386,6 @@ class TestPricingRule(FrappeTestCase): "price_or_product_discount": "Product", "same_item": 1, "free_qty": 1, - "enforce_free_item_qty": 1, "company": "_Test Company", } frappe.get_doc(test_record.copy()).insert() @@ -419,7 +418,6 @@ class TestPricingRule(FrappeTestCase): "same_item": 0, "free_item": "_Test Item 2", "free_qty": 1, - "enforce_free_item_qty": 1, "company": "_Test Company", } frappe.get_doc(test_record.copy()).insert() @@ -430,6 +428,54 @@ class TestPricingRule(FrappeTestCase): self.assertEqual(so.items[1].is_free_item, 1) self.assertEqual(so.items[1].item_code, "_Test Item 2") + def test_enforce_free_item_qty(self): + # this test is only for testing non-enforcement as all other tests in this file already test with enforcement + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule", + "apply_on": "Item Code", + "currency": "USD", + "items": [ + { + "item_code": "_Test Item", + } + ], + "selling": 1, + "rate_or_discount": "Discount Percentage", + "rate": 0, + "min_qty": 0, + "max_qty": 7, + "discount_percentage": 17.5, + "price_or_product_discount": "Product", + "same_item": 0, + "free_item": "_Test Item 2", + "free_qty": 1, + "company": "_Test Company", + } + pricing_rule = frappe.get_doc(test_record.copy()).insert() + + # With enforcement + so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True) + self.assertEqual(so.items[1].is_free_item, 1) + self.assertEqual(so.items[1].item_code, "_Test Item 2") + + # Test 1 : Saving a document with an item with pricing list without it's corresponding free item will cause it the free item to be refetched on save + so.items.pop(1) + so.save() + so.reload() + self.assertEqual(len(so.items), 2) + + # Without enforcement + pricing_rule.enforce_free_item_qty = 0 + pricing_rule.save() + + # Test 2 : Deleted free item will not be fetched again on save without enfrocement + so.items.pop(1) + so.save() + so.reload() + self.assertEqual(len(so.items), 1) + def test_cumulative_pricing_rule(self): frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule") test_record = { @@ -1113,7 +1159,6 @@ class TestPricingRule(FrappeTestCase): "price_or_product_discount": "Product", "same_item": 1, "free_qty": 1, - "enforce_free_item_qty": 1, "round_free_qty": 1, "is_recursive": 1, "recurse_for": 2, @@ -1159,7 +1204,6 @@ class TestPricingRule(FrappeTestCase): "price_or_product_discount": "Product", "same_item": 1, "free_qty": 10, - "enforce_free_item_qty": 1, "round_free_qty": 1, "is_recursive": 1, "recurse_for": 100, From fc2ec7c49577ee443f3196d47a98068d151a7712 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Feb 2025 16:29:01 +0530 Subject: [PATCH 39/58] fix: add is_new in if condition (cherry picked from commit 4dcac564863ee3fed391adccc5898f3cd07f4ccf) --- erpnext/accounts/doctype/pricing_rule/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index adadcceb7e7..33b62d49355 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -713,7 +713,9 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args): args.pop((item.item_code, item.pricing_rules)) for free_item in args.values(): - if frappe.get_value("Pricing Rule", free_item["pricing_rules"], "enforce_free_item_qty"): + if doc.is_new() or frappe.get_value( + "Pricing Rule", free_item["pricing_rules"], "enforce_free_item_qty" + ): doc.append("items", free_item) From f782900a15a44928e801c829f01b111ff03d9d06 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 17 Feb 2025 18:21:22 +0530 Subject: [PATCH 40/58] refactor: rename field (cherry picked from commit f3d598881c9b1135d5afca01059e27056a62b305) --- erpnext/accounts/doctype/pricing_rule/pricing_rule.json | 8 ++++---- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 4 ++-- .../accounts/doctype/pricing_rule/test_pricing_rule.py | 8 ++++---- erpnext/accounts/doctype/pricing_rule/utils.py | 4 ++-- erpnext/stock/doctype/pick_list/test_pick_list.py | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 3b743c796af..d1697f10903 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -53,7 +53,7 @@ "column_break_42", "free_item_uom", "round_free_qty", - "enforce_free_item_qty", + "dont_enforce_free_item_qty", "is_recursive", "recurse_for", "apply_recursion_over", @@ -648,15 +648,15 @@ { "default": "1", "depends_on": "eval:doc.price_or_product_discount == 'Product'", - "fieldname": "enforce_free_item_qty", + "fieldname": "dont_enforce_free_item_qty", "fieldtype": "Check", - "label": "Enforce Free Item Qty" + "label": "Don't Enforce Free Item Qty" } ], "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2025-02-05 21:03:22.103044", + "modified": "2025-02-17 18:15:39.824639", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 2d82144a50b..7a895f6818b 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -60,7 +60,7 @@ class PricingRule(Document): disable: DF.Check discount_amount: DF.Currency discount_percentage: DF.Float - enforce_free_item_qty: DF.Check + dont_enforce_free_item_qty: DF.Check for_price_list: DF.Link | None free_item: DF.Link | None free_item_rate: DF.Currency @@ -646,7 +646,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra if pricing_rule.margin_type in ["Percentage", "Amount"]: item_details.margin_rate_or_amount = 0.0 item_details.margin_type = None - elif pricing_rule.get("free_item") and pricing_rule.get("enforce_free_item_qty"): + elif pricing_rule.get("free_item") and not pricing_rule.get("dont_enforce_free_item_qty"): item_details.remove_free_item = ( item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item") ) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 75fb2632b17..3ad3d45ee47 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -428,7 +428,7 @@ class TestPricingRule(FrappeTestCase): self.assertEqual(so.items[1].is_free_item, 1) self.assertEqual(so.items[1].item_code, "_Test Item 2") - def test_enforce_free_item_qty(self): + def test_dont_enforce_free_item_qty(self): # this test is only for testing non-enforcement as all other tests in this file already test with enforcement frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") test_record = { @@ -467,10 +467,10 @@ class TestPricingRule(FrappeTestCase): self.assertEqual(len(so.items), 2) # Without enforcement - pricing_rule.enforce_free_item_qty = 0 + pricing_rule.dont_enforce_free_item_qty = 1 pricing_rule.save() - # Test 2 : Deleted free item will not be fetched again on save without enfrocement + # Test 2 : Deleted free item will not be fetched again on save without enforcement so.items.pop(1) so.save() so.reload() @@ -1499,7 +1499,7 @@ def make_pricing_rule(**args): "discount_amount": args.discount_amount or 0.0, "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0, "has_priority": args.has_priority or 0, - "enforce_free_item_qty": args.enforce_free_item_qty or 1, + "enforce_free_item_qty": args.dont_enforce_free_item_qty or 0, } ) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 33b62d49355..bbf73e80809 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -713,8 +713,8 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args): args.pop((item.item_code, item.pricing_rules)) for free_item in args.values(): - if doc.is_new() or frappe.get_value( - "Pricing Rule", free_item["pricing_rules"], "enforce_free_item_qty" + if doc.is_new() or not frappe.get_value( + "Pricing Rule", free_item["pricing_rules"], "dont_enforce_free_item_qty" ): doc.append("items", free_item) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 0f655460f41..c3043bbf1b5 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -1242,7 +1242,7 @@ class TestPickList(FrappeTestCase): "is_recursive": 1, "recurse_for": 2, "free_qty": 1, - "enforce_free_item_qty": 1, + "dont_enforce_free_item_qty": 0, "company": "_Test Company", "customer": "_Test Customer", } From 1abe1a1fd50bc6b860ebde52e56be07859fb8436 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 17 Feb 2025 18:47:35 +0530 Subject: [PATCH 41/58] fix: set default value to 0 as per new logic (cherry picked from commit 844f1636c0a82dd6e0d0e6e6e3a86bc6652e3ad9) --- erpnext/accounts/doctype/pricing_rule/pricing_rule.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index d1697f10903..c4825a6d519 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -646,7 +646,7 @@ "label": "Has Priority" }, { - "default": "1", + "default": "0", "depends_on": "eval:doc.price_or_product_discount == 'Product'", "fieldname": "dont_enforce_free_item_qty", "fieldtype": "Check", From 1e7c5ec0cb0cda95dabf704eb5b17761d98bab4f Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:21:29 +0530 Subject: [PATCH 42/58] fix: do not reschedule depreciation for fully depreciated asset on scrap (cherry picked from commit fd4c4f98fa9a96550d833b135cdd84ae93131805) --- erpnext/assets/doctype/asset/depreciation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 6ad0631135a..6a1410ac0f9 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -444,9 +444,9 @@ def scrap_asset(asset_name): notes = _("This schedule was created when Asset {0} was scrapped.").format( get_link_to_form(asset.doctype, asset.name) ) - - depreciate_asset(asset, date, notes) - asset.reload() + if asset.status != "Fully Depreciated": + depreciate_asset(asset, date, notes) + asset.reload() depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry") From e36b860a799537831d7e4d8be8b18b106f63dc12 Mon Sep 17 00:00:00 2001 From: Bhavansathru <122002510+Bhavan23@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:57:31 +0530 Subject: [PATCH 43/58] fix: fetch child account data for selected parent (#45904) * fix: fetch child account data for selected parent * fix: change reference name --------- Co-authored-by: venkat102 (cherry picked from commit 73e82b7afa1bef110194bee78e5edb31c6fe092c) --- .../trial_balance_for_party.js | 14 +-- .../trial_balance_for_party.py | 103 ++++++++++-------- 2 files changed, 63 insertions(+), 54 deletions(-) diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js index 50578d314e3..62482ac162c 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js @@ -68,16 +68,12 @@ frappe.query_reports["Trial Balance for Party"] = { { fieldname: "account", label: __("Account"), - fieldtype: "Link", + fieldtype: "MultiSelectList", options: "Account", - get_query: function () { - var company = frappe.query_report.get_filter_value("company"); - return { - doctype: "Account", - filters: { - company: company, - }, - }; + get_data: function (txt) { + return frappe.db.get_link_options("Account", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, }, { diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index dd1a12514e2..f6c79eb6c45 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -4,8 +4,10 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Sum from frappe.utils import cint, flt +from erpnext.accounts.report.general_ledger.general_ledger import get_accounts_with_children from erpnext.accounts.report.trial_balance.trial_balance import validate_filters @@ -35,9 +37,14 @@ def get_data(filters, show_party_name): filters=party_filters, order_by="name", ) + + account_filter = [] + if filters.get("account"): + account_filter = get_accounts_with_children(filters.get("account")) + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") - opening_balances = get_opening_balances(filters) - balances_within_period = get_balances_within_period(filters) + opening_balances = get_opening_balances(filters, account_filter) + balances_within_period = get_balances_within_period(filters, account_filter) data = [] # total_debit, total_credit = 0, 0 @@ -89,30 +96,34 @@ def get_data(filters, show_party_name): return data -def get_opening_balances(filters): - account_filter = "" - if filters.get("account"): - account_filter = "and account = %s" % (frappe.db.escape(filters.get("account"))) +def get_opening_balances(filters, account_filter=None): + GL_Entry = frappe.qb.DocType("GL Entry") - gle = frappe.db.sql( - f""" - select party, sum(debit) as opening_debit, sum(credit) as opening_credit - from `tabGL Entry` - where company=%(company)s - and is_cancelled=0 - and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s)) - {account_filter} - group by party""", - { - "company": filters.company, - "from_date": filters.from_date, - "to_date": filters.to_date, - "party_type": filters.party_type, - }, - as_dict=True, + query = ( + frappe.qb.from_(GL_Entry) + .select( + GL_Entry.party, + Sum(GL_Entry.debit).as_("opening_debit"), + Sum(GL_Entry.credit).as_("opening_credit"), + ) + .where( + (GL_Entry.company == filters.company) + & (GL_Entry.is_cancelled == 0) + & (GL_Entry.party_type == filters.party_type) + & (GL_Entry.party != "") + & ( + (GL_Entry.posting_date < filters.from_date) + | ((GL_Entry.is_opening == "Yes") & (GL_Entry.posting_date <= filters.to_date)) + ) + ) + .groupby(GL_Entry.party) ) + if account_filter: + query = query.where(GL_Entry.account.isin(account_filter)) + + gle = query.run(as_dict=True) + opening = frappe._dict() for d in gle: opening_debit, opening_credit = toggle_debit_credit(d.opening_debit, d.opening_credit) @@ -121,31 +132,33 @@ def get_opening_balances(filters): return opening -def get_balances_within_period(filters): - account_filter = "" - if filters.get("account"): - account_filter = "and account = %s" % (frappe.db.escape(filters.get("account"))) +def get_balances_within_period(filters, account_filter=None): + GL_Entry = frappe.qb.DocType("GL Entry") - gle = frappe.db.sql( - f""" - select party, sum(debit) as debit, sum(credit) as credit - from `tabGL Entry` - where company=%(company)s - and is_cancelled = 0 - and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and posting_date >= %(from_date)s and posting_date <= %(to_date)s - and ifnull(is_opening, 'No') = 'No' - {account_filter} - group by party""", - { - "company": filters.company, - "from_date": filters.from_date, - "to_date": filters.to_date, - "party_type": filters.party_type, - }, - as_dict=True, + query = ( + frappe.qb.from_(GL_Entry) + .select( + GL_Entry.party, + Sum(GL_Entry.debit).as_("debit"), + Sum(GL_Entry.credit).as_("credit"), + ) + .where( + (GL_Entry.company == filters.company) + & (GL_Entry.is_cancelled == 0) + & (GL_Entry.party_type == filters.party_type) + & (GL_Entry.party != "") + & (GL_Entry.posting_date >= filters.from_date) + & (GL_Entry.posting_date <= filters.to_date) + & (GL_Entry.is_opening == "No") + ) + .groupby(GL_Entry.party) ) + if account_filter: + query = query.where(GL_Entry.account.isin(account_filter)) + + gle = query.run(as_dict=True) + balances_within_period = frappe._dict() for d in gle: balances_within_period.setdefault(d.party, [d.debit, d.credit]) From 281431e041090d50ebff43e76b114a2608f6d4bc Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:05:02 +0530 Subject: [PATCH 44/58] fix: make purchase_receipt_item and purchase_invoice_item fields of data type (cherry picked from commit 8af9dcb33e954429a5c999213b1a1985568dcb38) # Conflicts: # erpnext/assets/doctype/asset/asset.json --- erpnext/assets/doctype/asset/asset.json | 44 ++++++++++++++++++++++--- erpnext/assets/doctype/asset/asset.py | 4 +-- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index e28eab9ed13..2683cf7ca96 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -541,17 +541,51 @@ }, { "fieldname": "purchase_receipt_item", - "fieldtype": "Link", + "fieldtype": "Data", "hidden": 1, - "label": "Purchase Receipt Item", - "options": "Purchase Receipt Item" + "label": "Purchase Receipt Item" }, { "fieldname": "purchase_invoice_item", - "fieldtype": "Link", + "fieldtype": "Data", "hidden": 1, +<<<<<<< HEAD "label": "Purchase Invoice Item", "options": "Purchase Invoice Item" +======= + "label": "Purchase Invoice Item" + }, + { + "fieldname": "insurance_details_tab", + "fieldtype": "Tab Break", + "label": "Insurance" + }, + { + "fieldname": "other_info_tab", + "fieldtype": "Tab Break", + "label": "Other Info" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "depreciation_tab", + "fieldtype": "Tab Break", + "label": "Depreciation" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "section_break_jtou", + "fieldtype": "Section Break", + "label": "Additional Info" +>>>>>>> 8af9dcb33e (fix: make purchase_receipt_item and purchase_invoice_item fields of data type) } ], "idx": 72, @@ -595,7 +629,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2024-12-26 14:23:20.968882", + "modified": "2025-02-11 16:01:56.140904", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 994f36ae1f2..20cc65e8e5d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -95,9 +95,9 @@ class Asset(AccountsController): purchase_amount: DF.Currency purchase_date: DF.Date | None purchase_invoice: DF.Link | None - purchase_invoice_item: DF.Link | None + purchase_invoice_item: DF.Data | None purchase_receipt: DF.Link | None - purchase_receipt_item: DF.Link | None + purchase_receipt_item: DF.Data | None split_from: DF.Link | None status: DF.Literal[ "Draft", From 87f337b6053bc20f7c83960a802af27596080552 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:17:42 +0530 Subject: [PATCH 45/58] fix: link correct row item of purchase doc (cherry picked from commit da1b4cb9abcfda788bbc1dd2162d33823b1313cf) --- erpnext/assets/doctype/asset/asset.js | 75 ++++++++++--------------- erpnext/assets/doctype/asset/asset.json | 6 +- erpnext/assets/doctype/asset/asset.py | 54 ++++++++++++++++++ 3 files changed, 87 insertions(+), 48 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 21e307b480c..769356843db 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -609,9 +609,7 @@ frappe.ui.form.on("Asset", { frm.trigger("toggle_reference_doc"); if (frm.doc.purchase_receipt) { if (frm.doc.item_code) { - frappe.db.get_doc("Purchase Receipt", frm.doc.purchase_receipt).then((pr_doc) => { - frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt", pr_doc); - }); + frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt"); } else { frm.set_value("purchase_receipt", ""); frappe.msgprint({ @@ -626,9 +624,7 @@ frappe.ui.form.on("Asset", { frm.trigger("toggle_reference_doc"); if (frm.doc.purchase_invoice) { if (frm.doc.item_code) { - frappe.db.get_doc("Purchase Invoice", frm.doc.purchase_invoice).then((pi_doc) => { - frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice", pi_doc); - }); + frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice"); } else { frm.set_value("purchase_invoice", ""); frappe.msgprint({ @@ -639,45 +635,36 @@ frappe.ui.form.on("Asset", { } }, - set_values_from_purchase_doc: function (frm, doctype, purchase_doc) { - frm.set_value("company", purchase_doc.company); - if (purchase_doc.bill_date) { - frm.set_value("purchase_date", purchase_doc.bill_date); - } else { - frm.set_value("purchase_date", purchase_doc.posting_date); - } - if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) { - frm.set_value("available_for_use_date", frm.doc.purchase_date); - } - const item = purchase_doc.items.find((item) => item.item_code === frm.doc.item_code); - if (!item) { - let doctype_field = frappe.scrub(doctype); - frm.set_value(doctype_field, ""); - frappe.msgprint({ - title: __("Invalid {0}", [__(doctype)]), - message: __("The selected {0} does not contain the selected Asset Item.", [__(doctype)]), - indicator: "red", - }); - } - frappe.db.get_value("Item", item.item_code, "is_grouped_asset", (r) => { - var asset_quantity = r.is_grouped_asset ? item.qty : 1; - var purchase_amount = flt( - item.valuation_rate * asset_quantity, - precision("gross_purchase_amount") - ); + set_values_from_purchase_doc: (frm, doctype) => { + frappe.call({ + method: "erpnext.assets.doctype.asset.asset.get_values_from_purchase_doc", + args: { + purchase_doc_name: frm.doc.purchase_receipt || frm.doc.purchase_invoice, + item_code: frm.doc.item_code, + doctype: doctype, + }, + callback: (r) => { + if (r.message) { + let data = r.message; + frm.set_value("company", data.company); + frm.set_value("purchase_date", data.purchase_date); + frm.set_value("gross_purchase_amount", data.gross_purchase_amount); + frm.set_value("purchase_amount", data.gross_purchase_amount); + frm.set_value("asset_quantity", data.asset_quantity); + frm.set_value("cost_center", data.cost_center); + frm.set_value("location", data.asset_location); - frm.set_value("gross_purchase_amount", purchase_amount); - frm.set_value("purchase_amount", purchase_amount); - frm.set_value("asset_quantity", asset_quantity); - frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center); - if (item.asset_location) { - frm.set_value("location", item.asset_location); - } - if (doctype === "Purchase Receipt") { - frm.set_value("purchase_receipt_item", item.name); - } else if (doctype === "Purchase Invoice") { - frm.set_value("purchase_invoice_item", item.name); - } + if (doctype === "Purchase Receipt") { + frm.set_value("purchase_receipt_item", data.purchase_receipt_item); + } else { + frm.set_value("purchase_invoice_item", data.purchase_invoice_item); + } + + let is_editable = !data.is_multiple_items; // if multiple items, then fields should not be read-only + frm.set_df_property("gross_purchase_amount", "read_only", is_editable); + frm.set_df_property("asset_quantity", "read_only", !is_editable); + } + }, }); }, diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 2683cf7ca96..9d33d5b6e22 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -227,8 +227,7 @@ "fieldtype": "Currency", "label": "Gross Purchase Amount", "mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)", - "options": "Company:company:default_currency", - "read_only_depends_on": "eval:!doc.is_existing_asset" + "options": "Company:company:default_currency" }, { "fieldname": "available_for_use_date", @@ -470,8 +469,7 @@ "default": "1", "fieldname": "asset_quantity", "fieldtype": "Int", - "label": "Asset Quantity", - "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" + "label": "Asset Quantity" }, { "fieldname": "depr_entry_posting_status", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 20cc65e8e5d..15ed3d0723a 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -121,6 +121,7 @@ class Asset(AccountsController): def validate(self): self.validate_precision() + self.set_purchase_doc_row_item() self.validate_asset_values() self.validate_asset_and_reference() self.validate_item() @@ -199,6 +200,35 @@ class Asset(AccountsController): def after_delete(self): add_asset_activity(self.name, _("Asset deleted")) + def set_purchase_doc_row_item(self): + if self.is_existing_asset or self.is_composite_asset: + return + + self.purchase_amount = self.gross_purchase_amount + purchase_type = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice" + purchase_doc = self.purchase_receipt or self.purchase_invoice + + if not purchase_doc: + return + + purchase_doc = frappe.get_doc(purchase_type, purchase_doc) + + for item in purchase_doc.items: + if self.asset_quantity > 1: + if item.base_net_amount == self.gross_purchase_amount and item.qty == self.asset_quantity: + self.purchase_receipt_item = item.name if purchase_type == "Purchase Receipt" else None + self.purchase_invoice_item = item.name if purchase_type == "Purchase Invoice" else None + return + elif item.qty == self.asset_quantity: + self.purchase_receipt_item = item.name if purchase_type == "Purchase Receipt" else None + self.purchase_invoice_item = item.name if purchase_type == "Purchase Invoice" else None + return + else: + if item.base_net_rate == self.gross_purchase_amount: + self.purchase_receipt_item = item.name if purchase_type == "Purchase Receipt" else None + self.purchase_invoice_item = item.name if purchase_type == "Purchase Invoice" else None + return + def validate_asset_and_reference(self): if self.purchase_invoice or self.purchase_receipt: reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt" @@ -1125,6 +1155,30 @@ def has_active_capitalization(asset): return active_capitalizations > 0 +@frappe.whitelist() +def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype): + purchase_doc = frappe.get_doc(doctype, purchase_doc_name) + matching_items = [item for item in purchase_doc.items if item.item_code == item_code] + + if not matching_items: + frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}")) + + first_item = matching_items[0] + is_multiple_items = len(matching_items) > 1 + + return { + "company": purchase_doc.company, + "purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"), + "gross_purchase_amount": flt(first_item.base_net_amount), + "asset_quantity": first_item.qty, + "cost_center": first_item.cost_center or purchase_doc.get("cost_center"), + "asset_location": first_item.get("asset_location"), + "is_multiple_items": is_multiple_items, + "purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None, + "purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None, + } + + @frappe.whitelist() def split_asset(asset_name, split_qty): asset = frappe.get_doc("Asset", asset_name) From 6183b380890a3a5445ac3277d63ba945efd6738e Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:01:27 +0530 Subject: [PATCH 46/58] fix: validate if no matching item found (cherry picked from commit 44c1425e73b89b1f82f88e3b84743a630841f08e) --- erpnext/assets/doctype/asset/asset.js | 4 +-- erpnext/assets/doctype/asset/asset.py | 35 ++++++++++++++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 769356843db..d5ebbc9df28 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -660,9 +660,9 @@ frappe.ui.form.on("Asset", { frm.set_value("purchase_invoice_item", data.purchase_invoice_item); } - let is_editable = !data.is_multiple_items; // if multiple items, then fields should not be read-only + let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only frm.set_df_property("gross_purchase_amount", "read_only", is_editable); - frm.set_df_property("asset_quantity", "read_only", !is_editable); + frm.set_df_property("asset_quantity", "read_only", is_editable); } }, }); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 15ed3d0723a..d89a33f9d4e 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -205,29 +205,40 @@ class Asset(AccountsController): return self.purchase_amount = self.gross_purchase_amount - purchase_type = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice" + purchase_doc_type = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice" purchase_doc = self.purchase_receipt or self.purchase_invoice if not purchase_doc: return - purchase_doc = frappe.get_doc(purchase_type, purchase_doc) + linked_item = self.get_linked_item(purchase_doc_type, purchase_doc) + + if linked_item: + if purchase_doc_type == "Purchase Receipt": + self.purchase_receipt_item = linked_item + else: + self.purchase_invoice_item = linked_item + + def get_linked_item(self, purchase_doc_type, purchase_doc): + purchase_doc = frappe.get_doc(purchase_doc_type, purchase_doc) for item in purchase_doc.items: if self.asset_quantity > 1: if item.base_net_amount == self.gross_purchase_amount and item.qty == self.asset_quantity: - self.purchase_receipt_item = item.name if purchase_type == "Purchase Receipt" else None - self.purchase_invoice_item = item.name if purchase_type == "Purchase Invoice" else None - return + return item.name elif item.qty == self.asset_quantity: - self.purchase_receipt_item = item.name if purchase_type == "Purchase Receipt" else None - self.purchase_invoice_item = item.name if purchase_type == "Purchase Invoice" else None - return + return item.name else: - if item.base_net_rate == self.gross_purchase_amount: - self.purchase_receipt_item = item.name if purchase_type == "Purchase Receipt" else None - self.purchase_invoice_item = item.name if purchase_type == "Purchase Invoice" else None - return + if item.base_net_rate == self.gross_purchase_amount and item.qty == self.asset_quantity: + return item.name + + # If no matching item found, raise validation error + frappe.throw( + _( + "No matching item found in {0} with item code {1}. " + "Please verify the purchase details and ensure the correct amount and quantity is recorded." + ).format(purchase_doc_type, self.item_code) + ) def validate_asset_and_reference(self): if self.purchase_invoice or self.purchase_receipt: From a50956811079ca6b23e613dc8f5487b515285797 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:04:26 +0530 Subject: [PATCH 47/58] fix: reset location only if there is value in row item location field (cherry picked from commit 2bb79197aabe3dd5921066d6550632b8c32ad85b) --- erpnext/assets/doctype/asset/asset.js | 1 - erpnext/assets/doctype/asset/asset.py | 8 -------- 2 files changed, 9 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index d5ebbc9df28..b9ea888faf7 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -652,7 +652,6 @@ frappe.ui.form.on("Asset", { frm.set_value("purchase_amount", data.gross_purchase_amount); frm.set_value("asset_quantity", data.asset_quantity); frm.set_value("cost_center", data.cost_center); - frm.set_value("location", data.asset_location); if (doctype === "Purchase Receipt") { frm.set_value("purchase_receipt_item", data.purchase_receipt_item); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index d89a33f9d4e..9b3cb022bd8 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -232,14 +232,6 @@ class Asset(AccountsController): if item.base_net_rate == self.gross_purchase_amount and item.qty == self.asset_quantity: return item.name - # If no matching item found, raise validation error - frappe.throw( - _( - "No matching item found in {0} with item code {1}. " - "Please verify the purchase details and ensure the correct amount and quantity is recorded." - ).format(purchase_doc_type, self.item_code) - ) - def validate_asset_and_reference(self): if self.purchase_invoice or self.purchase_receipt: reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt" From f043b466964bad71dcf69aab4a5db04090be40df Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 13 Feb 2025 18:02:56 +0530 Subject: [PATCH 48/58] fix: patch for creating asset depreciation schedule records (cherry picked from commit 7324dcb7c873a9bcf78774a91c1cf72108eaba1f) # Conflicts: # erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py --- ...sset_depreciation_schedules_from_assets.py | 88 +++++++------------ 1 file changed, 32 insertions(+), 56 deletions(-) diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py index 523b559d734..b049efa2c30 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -1,74 +1,38 @@ import frappe +from frappe.utils import cstr def execute(): +<<<<<<< HEAD frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule") frappe.reload_doc("assets", "doctype", "Asset Finance Book") frappe.reload_doc("assets", "doctype", "Asset") assets = get_details_of_draft_or_submitted_depreciable_assets() +======= +>>>>>>> 7324dcb7c8 (fix: patch for creating asset depreciation schedule records) asset_finance_books_map = get_asset_finance_books_map() - asset_depreciation_schedules_map = get_asset_depreciation_schedules_map() - for asset in assets: - if not asset_depreciation_schedules_map.get(asset.name): + for key, fb_row in asset_finance_books_map.items(): + depreciation_schedules = asset_depreciation_schedules_map.get(key) + if not depreciation_schedules: continue - depreciation_schedules = asset_depreciation_schedules_map[asset.name] + asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(fb_row, fb_row) + asset_depr_schedule_doc.flags.ignore_validate = True + asset_depr_schedule_doc.insert() - for fb_row in asset_finance_books_map[asset.name]: - asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + if fb_row.docstatus == 1: + frappe.db.set_value( + "Asset Depreciation Schedule", + asset_depr_schedule_doc.name, + {"docstatus": 1, "status": "Active"}, + ) - asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, fb_row) - - asset_depr_schedule_doc.insert() - - if asset.docstatus == 1: - asset_depr_schedule_doc.submit() - - depreciation_schedules_of_fb_row = [ - ds for ds in depreciation_schedules if ds["finance_book_id"] == str(fb_row.idx) - ] - - update_depreciation_schedules(depreciation_schedules_of_fb_row, asset_depr_schedule_doc.name) - - -def get_details_of_draft_or_submitted_depreciable_assets(): - asset = frappe.qb.DocType("Asset") - - records = ( - frappe.qb.from_(asset) - .select( - asset.name, - asset.opening_accumulated_depreciation, - asset.gross_purchase_amount, - asset.opening_number_of_booked_depreciations, - asset.docstatus, - ) - .where(asset.calculate_depreciation == 1) - .where(asset.docstatus < 2) - ).run(as_dict=True) - - return records - - -def group_records_by_asset_name(records): - grouped_dict = {} - - for item in records: - key = next(iter(item.keys())) - value = item[key] - - if value not in grouped_dict: - grouped_dict[value] = [] - - del item["asset_name"] - - grouped_dict[value].append(item) - - return grouped_dict + update_depreciation_schedules(depreciation_schedules, asset_depr_schedule_doc.name) def get_asset_finance_books_map(): @@ -90,12 +54,20 @@ def get_asset_finance_books_map(): afb.expected_value_after_useful_life, afb.daily_prorata_based, afb.shift_based, + asset.docstatus, + asset.name, + asset.opening_accumulated_depreciation, + asset.gross_purchase_amount, + asset.opening_number_of_booked_depreciations, ) .where(asset.docstatus < 2) + .where(asset.calculate_depreciation == 1) .orderby(afb.idx) ).run(as_dict=True) - asset_finance_books_map = group_records_by_asset_name(records) + asset_finance_books_map = frappe._dict() + for d in records: + asset_finance_books_map.setdefault((d.asset_name, cstr(d.finance_book)), d) return asset_finance_books_map @@ -111,13 +83,17 @@ def get_asset_depreciation_schedules_map(): .select( asset.name.as_("asset_name"), ds.name, + ds.finance_book, ds.finance_book_id, ) .where(asset.docstatus < 2) + .where(asset.calculate_depreciation == 1) .orderby(ds.idx) ).run(as_dict=True) - asset_depreciation_schedules_map = group_records_by_asset_name(records) + asset_depreciation_schedules_map = frappe._dict() + for d in records: + asset_depreciation_schedules_map.setdefault((d.asset_name, cstr(d.finance_book)), []).append(d) return asset_depreciation_schedules_map From 6a577438aa93d00160fde18887e7bdc6a58ce3ed Mon Sep 17 00:00:00 2001 From: Venkatesh <47534423+venkat102@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:39:01 +0530 Subject: [PATCH 49/58] fix(pos profile): check company while validating mandatory accounting dimension (#45974) (cherry picked from commit 17a2f4429034a1b824226902975c4f8bc963b975) --- erpnext/accounts/doctype/pos_profile/pos_profile.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 3b5aa5f75bd..a8c4a4d8d72 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -76,7 +76,11 @@ class POSProfile(Document): def validate_accounting_dimensions(self): acc_dims = get_checks_for_pl_and_bs_accounts() for acc_dim in acc_dims: - if not self.get(acc_dim.fieldname) and (acc_dim.mandatory_for_pl or acc_dim.mandatory_for_bs): + if ( + self.company == acc_dim.company + and not self.get(acc_dim.fieldname) + and (acc_dim.mandatory_for_pl or acc_dim.mandatory_for_bs) + ): frappe.throw( _( "{0} is a mandatory Accounting Dimension.
" From 44e1ca9d0549b7401c8945a48fbf15f79867d81b Mon Sep 17 00:00:00 2001 From: Ejaaz Khan <67804911+iamejaaz@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:40:55 +0530 Subject: [PATCH 50/58] fix: round sum amount in JE auditing PF (#45961) (cherry picked from commit 941085000ad7e23be6adf64a897e358b4634eedb) --- .../journal_auditing_voucher/journal_auditing_voucher.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html index c1c611ee3a3..8a6968ee373 100644 --- a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html +++ b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html @@ -44,7 +44,7 @@ {% endfor %} Total (debit) - {{ gl | sum(attribute='debit') }} + {{ gl | sum(attribute='debit') | round(2) }} Credit @@ -61,7 +61,7 @@ {% endfor %} Total (credit) - {{ gl | sum(attribute='credit') }} + {{ gl | sum(attribute='credit') | round(2) }} Narration: {{ gl[0].remarks }} From 830edb8f524c23f749d80549bf0f92aa27c7ac18 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:43:53 +0530 Subject: [PATCH 51/58] fix: auto create asset due to message error (backport #45934) (#45952) fix: auto create asset due to message error (#45934) * fix: auto create asset due to message error * fix: linters (cherry picked from commit 6f1bc5225a42945b8e065827fe44c8f5d6004106) Co-authored-by: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com> --- erpnext/controllers/buying_controller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 8da22785b94..9643110b76a 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -779,8 +779,10 @@ class BuyingController(SubcontractingController): is_plural = "s" if len(created_assets) != 1 else "" messages.append( - _("Asset{} {assets_link} created for {}").format( - is_plural, frappe.bold(d.item_code), assets_link=assets_link + _("Asset{is_plural} {assets_link} created for {item_code}").format( + is_plural=is_plural, + assets_link=assets_link, + item_code=frappe.bold(d.item_code), ) ) else: From 1a4297ac35adcf36055ddc2619dc95ad257acb87 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:45:10 +0530 Subject: [PATCH 52/58] feat: added ability to use custom html format for process statement of accounts (copy #45746) (#46012) feat: added ability to use custom html format for process statement of accounts (#45746) * feat: added ability to use custom print format for process statement of accounts documents. * fix: handles missing hook issues * chore: linter changes --------- Co-authored-by: Boy4099 Co-authored-by: ruthra kumar (cherry picked from commit a0cd08e9ea07d69df664bb152faf4dd5dfac50e3) Co-authored-by: Steve Wilson --- .../process_statement_of_accounts.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index cee7a238df0..48f400e1a46 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -236,17 +236,21 @@ def get_ar_filters(doc, entry): def get_html(doc, filters, entry, col, res, ageing): base_template_path = "frappe/www/printview.html" - template_path = ( - "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" - if doc.report == "General Ledger" - else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html" - ) + template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html" + if doc.report == "General Ledger": + template_path = ( + "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" + ) + + process_soa_html = frappe.get_hooks("process_soa_html") + # fetching custom print format for Process Statement of Accounts + if process_soa_html and process_soa_html.get(doc.report): + template_path = process_soa_html[doc.report][-1] if doc.letter_head: from frappe.www.printview import get_letter_head letter_head = get_letter_head(doc, 0) - html = frappe.render_template( template_path, { @@ -262,7 +266,6 @@ def get_html(doc, filters, entry, col, res, ageing): else None, }, ) - html = frappe.render_template( base_template_path, {"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer}, From 84647a1c73c9dea83d1706e4a8ec7739414c0f12 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:47:40 +0530 Subject: [PATCH 53/58] fix: resolved conflicts --- erpnext/assets/doctype/asset/asset.json | 35 ------------------------- 1 file changed, 35 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 9d33d5b6e22..06b851f2cca 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -547,43 +547,8 @@ "fieldname": "purchase_invoice_item", "fieldtype": "Data", "hidden": 1, -<<<<<<< HEAD "label": "Purchase Invoice Item", "options": "Purchase Invoice Item" -======= - "label": "Purchase Invoice Item" - }, - { - "fieldname": "insurance_details_tab", - "fieldtype": "Tab Break", - "label": "Insurance" - }, - { - "fieldname": "other_info_tab", - "fieldtype": "Tab Break", - "label": "Other Info" - }, - { - "fieldname": "connections_tab", - "fieldtype": "Tab Break", - "label": "Connections", - "show_dashboard": 1 - }, - { - "fieldname": "depreciation_tab", - "fieldtype": "Tab Break", - "label": "Depreciation" - }, - { - "fieldname": "accounting_dimensions_section", - "fieldtype": "Section Break", - "label": "Accounting Dimensions" - }, - { - "fieldname": "section_break_jtou", - "fieldtype": "Section Break", - "label": "Additional Info" ->>>>>>> 8af9dcb33e (fix: make purchase_receipt_item and purchase_invoice_item fields of data type) } ], "idx": 72, From 9b8623dd64b5fab284831a137541d30ba98e3911 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:50:53 +0530 Subject: [PATCH 54/58] chore: resolved conflicts --- .../v15_0/create_asset_depreciation_schedules_from_assets.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py index b049efa2c30..4dc1ad8b6c0 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -3,15 +3,12 @@ from frappe.utils import cstr def execute(): -<<<<<<< HEAD frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule") frappe.reload_doc("assets", "doctype", "Asset Finance Book") frappe.reload_doc("assets", "doctype", "Asset") assets = get_details_of_draft_or_submitted_depreciable_assets() -======= ->>>>>>> 7324dcb7c8 (fix: patch for creating asset depreciation schedule records) asset_finance_books_map = get_asset_finance_books_map() asset_depreciation_schedules_map = get_asset_depreciation_schedules_map() From dd5d144b55c78e6819560da48544ed9888568149 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:00:31 +0530 Subject: [PATCH 55/58] fix: remove unused code --- .../create_asset_depreciation_schedules_from_assets.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py index 4dc1ad8b6c0..ff77fbb91ec 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -3,12 +3,6 @@ from frappe.utils import cstr def execute(): - frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule") - frappe.reload_doc("assets", "doctype", "Asset Finance Book") - frappe.reload_doc("assets", "doctype", "Asset") - - assets = get_details_of_draft_or_submitted_depreciable_assets() - asset_finance_books_map = get_asset_finance_books_map() asset_depreciation_schedules_map = get_asset_depreciation_schedules_map() From cbec989a7cdf15be0367e8d1b1d52ebeefc928f3 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Wed, 19 Feb 2025 12:23:10 +0530 Subject: [PATCH 56/58] fix(send_message): escape HTML in the text Signed-off-by: Akhil Narang (cherry picked from commit 448a5db20f2959fd6ce809a8894d32c8345a76fc) --- erpnext/templates/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/templates/utils.py b/erpnext/templates/utils.py index 57750a56f6f..15af9f0f014 100644 --- a/erpnext/templates/utils.py +++ b/erpnext/templates/utils.py @@ -3,6 +3,7 @@ import frappe +from frappe.utils import escape_html @frappe.whitelist(allow_guest=True) @@ -11,6 +12,8 @@ def send_message(sender, message, subject="Website Query"): website_send_message(sender, message, subject) + message = escape_html(message) + lead = customer = None customer = frappe.db.sql( """select distinct dl.link_name from `tabDynamic Link` dl From 15106b49b6d2101455a9244dcf0837b827f9ca01 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:51:09 +0530 Subject: [PATCH 57/58] fix: handle division by zero error (backport #45966) (#46015) fix: handle division by zero error (#45966) Co-authored-by: Sanket322 (cherry picked from commit 24394765a6f30d6b394a9ae2f7e065f60fbcb408) Co-authored-by: Sanket Shah <113279972+Sanket322@users.noreply.github.com> --- erpnext/controllers/status_updater.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index e1cd0a1c340..e1e4c4ce8f2 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate +from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate, safe_div class OverAllowanceError(frappe.ValidationError): @@ -543,7 +543,7 @@ class StatusUpdater(Document): )[0][0] ) - per_billed = (min(ref_doc_qty, billed_qty) / ref_doc_qty) * 100 + per_billed = safe_div(min(ref_doc_qty, billed_qty), ref_doc_qty) * 100 ref_doc = frappe.get_doc(ref_dt, ref_dn) From eead6d46ff8dbb8b76090d9bdbc03f033ecd5fbe Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:52:09 +0530 Subject: [PATCH 58/58] fix: remove public access to list items (backport #45838) (#46018) fix: remove public access to list items (cherry picked from commit 2bd596ee3d98d418ea74105eb49604df64877b8f) Co-authored-by: CaseSolved --- erpnext/stock/doctype/item/item.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 2c56b6a80bc..16ac4fd1017 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_guest_to_view": 1, "allow_import": 1, "allow_rename": 1, "autoname": "field:item_code", @@ -888,10 +887,9 @@ "icon": "fa fa-tag", "idx": 2, "image_field": "image", - "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2024-04-30 13:46:39.098753", + "modified": "2025-02-03 23:43:57.253667", "modified_by": "Administrator", "module": "Stock", "name": "Item",