From ad05e6dec24a9cb67cb649aa7e91ca1a03575f4d Mon Sep 17 00:00:00 2001 From: David Date: Wed, 28 Aug 2024 10:28:02 +0200 Subject: [PATCH 01/49] fix: distributed discounts on si (cherry picked from commit 0bab6f34c13095d67ffb9e485aebb161980d6da7) # Conflicts: # erpnext/buying/doctype/purchase_order_item/purchase_order_item.json # erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json # erpnext/selling/doctype/quotation_item/quotation_item.json # erpnext/selling/doctype/sales_order_item/sales_order_item.json # erpnext/stock/doctype/delivery_note_item/delivery_note_item.json --- .../pos_invoice_item/pos_invoice_item.json | 7 +++++ .../pos_invoice_item/pos_invoice_item.py | 1 + .../purchase_invoice_item.json | 9 ++++++- .../purchase_invoice_item.py | 1 + .../sales_invoice_item.json | 11 ++++++-- .../sales_invoice_item/sales_invoice_item.py | 1 + .../purchase_order_item.json | 27 ++++++++++++++++++- .../purchase_order_item.py | 1 + .../supplier_quotation_item.json | 11 ++++++++ .../supplier_quotation_item.py | 1 + erpnext/controllers/accounts_controller.py | 7 +++-- erpnext/controllers/taxes_and_totals.py | 7 +++++ .../quotation_item/quotation_item.json | 13 ++++++++- .../doctype/quotation_item/quotation_item.py | 1 + .../sales_order_item/sales_order_item.json | 13 ++++++++- .../sales_order_item/sales_order_item.py | 1 + .../delivery_note_item.json | 13 ++++++++- .../delivery_note_item/delivery_note_item.py | 1 + .../purchase_receipt_item.json | 9 ++++++- .../purchase_receipt_item.py | 1 + 20 files changed, 126 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 828fc30db6e..799a112c0c1 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -37,6 +37,7 @@ "column_break_19", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "section_break1", "rate", @@ -847,6 +848,12 @@ { "fieldname": "column_break_ciit", "fieldtype": "Column Break" + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" } ], "istable": 1, diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py index c24db1d6a03..429f340a4f5 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py @@ -39,6 +39,7 @@ class POSInvoiceItem(Document): description: DF.TextEditor discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency dn_detail: DF.Data | None enable_deferred_revenue: DF.Check expense_account: DF.Link | None diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 8a2ba36cf62..12ed168875b 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -38,6 +38,7 @@ "column_break_30", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "sec_break2", "rate", @@ -838,7 +839,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "section_break_26", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -969,6 +970,12 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1 + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" } ], "idx": 1, diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py index a8f844c6c1c..96db9d66f05 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py @@ -34,6 +34,7 @@ class PurchaseInvoiceItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency enable_deferred_expense: DF.Check expense_account: DF.Link | None from_warehouse: DF.Link | None diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 932bc8e49d4..7d1e4abdbc5 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -37,6 +37,7 @@ "column_break_19", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "section_break1", "rate", @@ -253,7 +254,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -922,12 +923,18 @@ { "fieldname": "column_break_ytgd", "fieldtype": "Column Break" + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-05-23 16:36:18.970862", + "modified": "2024-06-02 06:14:40.009020", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py index 9be1b42aab3..28d468ac736 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py @@ -39,6 +39,7 @@ class SalesInvoiceItem(Document): discount_account: DF.Link | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency dn_detail: DF.Data | None enable_deferred_revenue: DF.Check expense_account: DF.Link | None 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 e3e8def7ffd..cb649563fbe 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -42,6 +42,7 @@ "column_break_28", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "sec_break2", "rate", @@ -780,7 +781,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin_section", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -909,13 +910,37 @@ { "fieldname": "column_break_fyqr", "fieldtype": "Column Break" +<<<<<<< HEAD +======= + }, + { + "fieldname": "column_break_pjyo", + "fieldtype": "Column Break" + }, + { + "fieldname": "job_card", + "fieldtype": "Link", + "label": "Job Card", + "options": "Job Card", + "search_index": 1 + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" +>>>>>>> 0bab6f34c1 (fix: distributed discounts on si) } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2024-02-05 11:23:24.859435", +======= + "modified": "2024-06-02 06:20:10.508290", +>>>>>>> 0bab6f34c1 (fix: distributed discounts on si) "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py index e9cc2b4eecf..79c66c3a30e 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py @@ -37,6 +37,7 @@ class PurchaseOrderItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency expected_delivery_date: DF.Date | None expense_account: DF.Link | None fg_item: DF.Link | None diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index a6229b5950b..71e8da47340 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -32,6 +32,7 @@ "price_list_rate", "discount_percentage", "discount_amount", + "distributed_discount_amount", "col_break_price_list", "base_price_list_rate", "sec_break1", @@ -565,13 +566,23 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-11-17 12:25:26.235367", +======= + "modified": "2024-06-02 06:22:17.864822", +>>>>>>> 0bab6f34c1 (fix: distributed discounts on si) "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py index d2f4a59930b..a51b9500fd8 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py @@ -26,6 +26,7 @@ class SupplierQuotationItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency expected_delivery_date: DF.Date | None image: DF.Attach | None is_free_item: DF.Check diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ce5d813b801..32f11804206 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1649,8 +1649,11 @@ class AccountsController(TransactionBase): and self.get("discount_amount") and self.get("additional_discount_account") ): - amount = item.amount - base_amount = item.base_amount + amount += item.distributed_discount_amount + base_amount += flt( + item.distributed_discount_amount * self.get("conversion_rate"), + item.precision("distributed_discount_amount"), + ) return amount, base_amount diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2d3b224b76f..ebf10977b52 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -674,6 +674,9 @@ class calculate_taxes_and_totals: ) item.net_amount = flt(item.net_amount - distributed_amount, item.precision("net_amount")) + item.distributed_discount_amount = flt( + distributed_amount, item.precision("distributed_discount_amount") + ) net_total += item.net_amount # discount amount rounding loss adjustment if no taxes @@ -690,6 +693,10 @@ class calculate_taxes_and_totals: item.net_amount = flt( item.net_amount + discount_amount_loss, item.precision("net_amount") ) + item.distributed_discount_amount = flt( + distributed_amount + discount_amount_loss, + item.precision("distributed_discount_amount"), + ) item.net_rate = ( flt(item.net_amount / item.qty, item.precision("net_rate")) if item.qty else 0 diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 0e25313f76a..13eda8b4c88 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -34,6 +34,7 @@ "column_break_18", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "section_break1", "rate", @@ -235,7 +236,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -662,12 +663,22 @@ "label": "Has Alternative Item", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" } ], "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-11-14 18:24:24.619832", +======= + "modified": "2024-06-02 06:21:09.508680", +>>>>>>> 0bab6f34c1 (fix: distributed discounts on si) "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.py b/erpnext/selling/doctype/quotation_item/quotation_item.py index f209762c3ba..4aeb1ba0ec8 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.py +++ b/erpnext/selling/doctype/quotation_item/quotation_item.py @@ -32,6 +32,7 @@ class QuotationItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency gross_profit: DF.Currency has_alternative_item: DF.Check image: DF.Attach | None 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 d451768eaab..ba183586bcd 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -40,6 +40,7 @@ "column_break_19", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "section_break_simple1", "rate", @@ -280,7 +281,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -905,12 +906,22 @@ "label": "Is Stock Item", "print_hide": 1, "report_hide": 1 + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" } ], "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2024-03-21 18:15:56.625005", +======= + "modified": "2024-06-02 06:13:40.597947", +>>>>>>> 0bab6f34c1 (fix: distributed discounts on si) "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 fa7b9b968f3..58be64bce21 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -38,6 +38,7 @@ class SalesOrderItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency ensure_delivery_based_on_produced_serial_no: DF.Check grant_commission: DF.Check gross_profit: DF.Currency diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index b8164b25753..136dd2a7572 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -40,6 +40,7 @@ "column_break_19", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "section_break_1", "rate", @@ -274,7 +275,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -907,13 +908,23 @@ { "fieldname": "column_break_rxvc", "fieldtype": "Column Break" + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2024-03-21 18:15:07.603672", +======= + "modified": "2024-06-02 06:18:38.491763", +>>>>>>> 0bab6f34c1 (fix: distributed discounts on si) "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py index b76f7429728..f9597f8b19e 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py @@ -36,6 +36,7 @@ class DeliveryNoteItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Float + distributed_discount_amount: DF.Currency dn_detail: DF.Data | None expense_account: DF.Link | None grant_commission: DF.Check diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 610bceddf0f..f3e3adee260 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -48,6 +48,7 @@ "column_break_37", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "sec_break1", "rate", @@ -911,7 +912,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin_section", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -1135,6 +1136,12 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1 + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" } ], "idx": 1, diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py index 2154007771d..6581fc00d37 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py @@ -36,6 +36,7 @@ class PurchaseReceiptItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency expense_account: DF.Link | None from_warehouse: DF.Link | None has_item_scanned: DF.Check From 6f6574c5acc2f9cd673725bdf92637f73b91718f Mon Sep 17 00:00:00 2001 From: "David (aider)" Date: Fri, 30 Aug 2024 09:28:21 +0200 Subject: [PATCH 02/49] feat: add unit tests for distributed_discount_amount (cherry picked from commit a464bd861ba72df53b87ebea23c0ed695ec13aee) --- .../tests/test_distributed_discount.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 erpnext/controllers/tests/test_distributed_discount.py diff --git a/erpnext/controllers/tests/test_distributed_discount.py b/erpnext/controllers/tests/test_distributed_discount.py new file mode 100644 index 00000000000..e4dbdbb1480 --- /dev/null +++ b/erpnext/controllers/tests/test_distributed_discount.py @@ -0,0 +1,61 @@ +from frappe.tests.utils import FrappeTestCase + +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + +class TestTaxesAndTotals(AccountsTestMixin, FrappeTestCase): + def test_distributed_discount_amount(self): + so = make_sales_order(do_not_save=1) + so.apply_discount_on = "Net Total" + so.discount_amount = 100 + so.items[0].qty = 5 + so.items[0].rate = 100 + so.append("items", so.items[0].as_dict()) + so.items[1].qty = 5 + so.items[1].rate = 200 + so.save() + + calculate_taxes_and_totals(so) + + self.assertAlmostEqual(so.items[0].distributed_discount_amount, 33.33, places=2) + self.assertAlmostEqual(so.items[1].distributed_discount_amount, 66.67, places=2) + self.assertAlmostEqual(so.items[0].net_amount, 466.67, places=2) + self.assertAlmostEqual(so.items[1].net_amount, 933.33, places=2) + self.assertEqual(so.total, 1500) + self.assertEqual(so.net_total, 1400) + self.assertEqual(so.grand_total, 1400) + + def test_distributed_discount_amount_with_taxes(self): + so = make_sales_order(do_not_save=1) + so.apply_discount_on = "Grand Total" + so.discount_amount = 100 + so.items[0].qty = 5 + so.items[0].rate = 100 + so.append("items", so.items[0].as_dict()) + so.items[1].qty = 5 + so.items[1].rate = 200 + so.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "included_in_print_rate": True, + "rate": 10, + }, + ) + so.save() + + calculate_taxes_and_totals(so) + + # like in test_distributed_discount_amount, but reduced by the included tax + self.assertAlmostEqual(so.items[0].distributed_discount_amount, 33.33 / 1.1, places=2) + self.assertAlmostEqual(so.items[1].distributed_discount_amount, 66.67 / 1.1, places=2) + self.assertAlmostEqual(so.items[0].net_amount, 466.67 / 1.1, places=2) + self.assertAlmostEqual(so.items[1].net_amount, 933.33 / 1.1, places=2) + self.assertEqual(so.total, 1500) + self.assertAlmostEqual(so.net_total, 1272.73, places=2) + self.assertEqual(so.grand_total, 1400) From 7ab81b7e546c633a1902e24968238ee9e87ec629 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:14:03 +0100 Subject: [PATCH 03/49] fix(Employee): remove User Permissions if create_user_permission is unchecked --- erpnext/setup/doctype/employee/employee.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 93af08d365f..42dc1df96bb 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -5,6 +5,7 @@ from frappe import _, scrub, throw from frappe.model.naming import set_name_by_naming_series from frappe.permissions import ( add_user_permission, + delete_user_permission, get_doc_permissions, has_permission, remove_user_permission, @@ -85,20 +86,19 @@ class Employee(NestedSet): self.reset_employee_emails_cache() def update_user_permissions(self): - if not self.create_user_permission: - return - if not has_permission("User Permission", ptype="write", raise_exception=False): + if not has_permission("User Permission", ptype="write", print_logs=False): return employee_user_permission_exists = frappe.db.exists( "User Permission", {"allow": "Employee", "for_value": self.name, "user": self.user_id} ) - if employee_user_permission_exists: - return - - add_user_permission("Employee", self.name, self.user_id) - add_user_permission("Company", self.company, self.user_id) + if employee_user_permission_exists and not self.create_user_permission: + delete_user_permission("Employee", self.name, self.user_id) + delete_user_permission("Company", self.company, self.user_id) + elif not employee_user_permission_exists and self.create_user_permission: + add_user_permission("Employee", self.name, self.user_id) + add_user_permission("Company", self.company, self.user_id) def update_user(self): # add employee role if missing From e47d07d98ccf8eab3c8590043e38d00a3fc1fcc8 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:28:40 +0100 Subject: [PATCH 04/49] chore: use existing utility function --- erpnext/setup/doctype/employee/employee.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 42dc1df96bb..8941e4e984d 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -5,7 +5,6 @@ from frappe import _, scrub, throw from frappe.model.naming import set_name_by_naming_series from frappe.permissions import ( add_user_permission, - delete_user_permission, get_doc_permissions, has_permission, remove_user_permission, @@ -94,8 +93,8 @@ class Employee(NestedSet): ) if employee_user_permission_exists and not self.create_user_permission: - delete_user_permission("Employee", self.name, self.user_id) - delete_user_permission("Company", self.company, self.user_id) + remove_user_permission("Employee", self.name, self.user_id) + remove_user_permission("Company", self.company, self.user_id) elif not employee_user_permission_exists and self.create_user_permission: add_user_permission("Employee", self.name, self.user_id) add_user_permission("Company", self.company, self.user_id) From b0f3d62dd0da7c06964a95cd61a5b115794b518d Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:47:17 +0100 Subject: [PATCH 05/49] fix: only update User Permissions if a relevant field has changed --- erpnext/setup/doctype/employee/employee.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 8941e4e984d..c7ecc3bc5fd 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -85,7 +85,10 @@ class Employee(NestedSet): self.reset_employee_emails_cache() def update_user_permissions(self): - if not has_permission("User Permission", ptype="write", print_logs=False): + if ( + not has_permission("User Permission", ptype="write", print_logs=False) + or (not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission")) + ): return employee_user_permission_exists = frappe.db.exists( From b0e8f85a27fd4f8a0cd178b35de609c5b1820536 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:51:36 +0100 Subject: [PATCH 06/49] refactor: make linter happy --- erpnext/setup/doctype/employee/employee.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index c7ecc3bc5fd..b50fbafa17a 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -85,9 +85,8 @@ class Employee(NestedSet): self.reset_employee_emails_cache() def update_user_permissions(self): - if ( - not has_permission("User Permission", ptype="write", print_logs=False) - or (not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission")) + if not has_permission("User Permission", ptype="write", print_logs=False) or ( + not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission") ): return From 2431141062706bd6979a61fac5194d0e593d2037 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:07:44 +0200 Subject: [PATCH 07/49] fix: remove invalid parameter --- erpnext/setup/doctype/employee/employee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index b50fbafa17a..41bc41d5d34 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -85,7 +85,7 @@ class Employee(NestedSet): self.reset_employee_emails_cache() def update_user_permissions(self): - if not has_permission("User Permission", ptype="write", print_logs=False) or ( + if not has_permission("User Permission", ptype="write") or ( not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission") ): return From 73683b275457279402d060fb9e246662b7de2b7b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 8 Apr 2025 15:02:12 +0530 Subject: [PATCH 08/49] fix: group sub assemblies in production plan (cherry picked from commit f58abed935f17f7d4c6b79c1c097954ce5a0643b) --- .../production_plan/production_plan.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index cfd3f789e78..5330001c617 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -939,6 +939,7 @@ class ProductionPlan(Document): bom_data = [] get_sub_assembly_items( + [item.production_item for item in sub_assembly_items_store], row.bom_no, bom_data, row.planned_qty, @@ -1528,10 +1529,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d so_item_details = frappe._dict() - sub_assembly_items = {} + sub_assembly_items = defaultdict(int) if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"): for d in doc.get("sub_assembly_items"): - sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty")) + sub_assembly_items[(d.get("production_item"), d.get("bom_no"))] += d.get("qty") for data in po_items: if not data.get("include_exploded_items") and doc.get("sub_assembly_items"): @@ -1560,6 +1561,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d item_details = {} if doc.get("sub_assembly_items"): item_details = get_raw_materials_of_sub_assembly_items( + so_item_details[doc.get("sales_order")].keys() if so_item_details else [], item_details, company, bom_no, @@ -1737,6 +1739,7 @@ def get_item_data(item_code): def get_sub_assembly_items( + sub_assembly_items, bom_no, bom_data, to_produce_qty, @@ -1752,7 +1755,7 @@ def get_sub_assembly_items( stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) bin_details = frappe._dict() - if skip_available_sub_assembly_item: + if skip_available_sub_assembly_item and d.item_code not in sub_assembly_items: bin_details = get_bin_details(d, company, for_warehouse=warehouse) for _bin_dict in bin_details: @@ -1787,6 +1790,7 @@ def get_sub_assembly_items( if d.value: get_sub_assembly_items( + sub_assembly_items, d.value, bom_data, stock_qty, @@ -1866,7 +1870,13 @@ def get_non_completed_production_plans(): def get_raw_materials_of_sub_assembly_items( - item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1 + existing_sub_assembly_items, + item_details, + company, + bom_no, + include_non_stock_items, + sub_assembly_items, + planned_qty=1, ): bei = frappe.qb.DocType("BOM Item") bom = frappe.qb.DocType("BOM") @@ -1910,12 +1920,13 @@ def get_raw_materials_of_sub_assembly_items( for item in items: key = (item.item_code, item.bom_no) - if item.bom_no and key not in sub_assembly_items: + if (item.bom_no and key not in sub_assembly_items) or (item.item_code in existing_sub_assembly_items): continue if item.bom_no: planned_qty = flt(sub_assembly_items[key]) get_raw_materials_of_sub_assembly_items( + existing_sub_assembly_items, item_details, company, item.bom_no, From b3e852adfcfdb522152aad235e38c08dae2d0053 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 11 Apr 2025 16:22:28 +0530 Subject: [PATCH 09/49] fix: logic and added test case (cherry picked from commit f071255340bfb6fa2dd1c2f58d819c73c36ee6ae) --- .../production_plan/production_plan.py | 18 ++++-- .../production_plan/test_production_plan.py | 58 +++++++++++++++++++ 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 5330001c617..2d5dfbf32fe 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -925,6 +925,7 @@ class ProductionPlan(Document): "Fetch sub assembly items and optionally combine them." self.sub_assembly_items = [] sub_assembly_items_store = [] # temporary store to process all subassembly items + bin_details = frappe._dict() for row in self.po_items: if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse: @@ -940,6 +941,7 @@ class ProductionPlan(Document): get_sub_assembly_items( [item.production_item for item in sub_assembly_items_store], + bin_details, row.bom_no, bom_data, row.planned_qty, @@ -1740,6 +1742,7 @@ def get_item_data(item_code): def get_sub_assembly_items( sub_assembly_items, + bin_details, bom_no, bom_data, to_produce_qty, @@ -1754,25 +1757,27 @@ def get_sub_assembly_items( parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) - bin_details = frappe._dict() if skip_available_sub_assembly_item and d.item_code not in sub_assembly_items: - bin_details = get_bin_details(d, company, for_warehouse=warehouse) + bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse)) - for _bin_dict in bin_details: + for _bin_dict in bin_details[d.item_code]: if _bin_dict.projected_qty > 0: - if _bin_dict.projected_qty > stock_qty: + if _bin_dict.projected_qty >= stock_qty: + _bin_dict.projected_qty -= stock_qty stock_qty = 0 continue else: stock_qty = stock_qty - _bin_dict.projected_qty elif warehouse: - bin_details = get_bin_details(d, company, for_warehouse=warehouse) + bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse)) if stock_qty > 0: bom_data.append( frappe._dict( { - "actual_qty": bin_details[0].get("actual_qty", 0) if bin_details else 0, + "actual_qty": bin_details[d.item_code][0].get("actual_qty", 0) + if bin_details + else 0, "parent_item_code": parent_item_code, "description": d.description, "production_item": d.item_code, @@ -1791,6 +1796,7 @@ def get_sub_assembly_items( if d.value: get_sub_assembly_items( sub_assembly_items, + bin_details, d.value, bom_data, stock_qty, diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index c7228823bed..e5b60c7a4e6 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1635,6 +1635,64 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(row.production_item, sf_item) self.assertEqual(row.qty, 5.0) + def test_calculation_of_sub_assembly_items(self): + make_item("Sub Assembly Item ", properties={"is_stock_item": 1}) + make_item("RM Item 1", properties={"is_stock_item": 1}) + make_item("RM Item 2", properties={"is_stock_item": 1}) + make_bom(item="Sub Assembly Item", raw_materials=["RM Item 1", "RM Item 2"]) + make_bom(item="_Test FG Item", raw_materials=["Sub Assembly Item", "RM Item 1"]) + make_bom(item="_Test FG Item 2", raw_materials=["Sub Assembly Item"]) + + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + make_stock_entry( + item_code="Sub Assembly Item", + qty=80, + purpose="Material Receipt", + to_warehouse="_Test Warehouse - _TC", + ) + make_stock_entry( + item_code="RM Item 1", qty=90, purpose="Material Receipt", to_warehouse="_Test Warehouse - _TC" + ) + + plan = create_production_plan( + skip_available_sub_assembly_item=1, + sub_assembly_warehouse="_Test Warehouse - _TC", + warehouse="_Test Warehouse - _TC", + item_code="_Test FG Item", + skip_getting_mr_items=1, + planned_qty=100, + do_not_save=1, + ) + plan.get_items_from = "" + plan.append( + "po_items", + { + "use_multi_level_bom": 1, + "item_code": "_Test FG Item 2", + "bom_no": frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"), + "planned_qty": 50, + "planned_start_date": now_datetime(), + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + ) + plan.save() + + plan.get_sub_assembly_items() + + self.assertEqual(plan.sub_assembly_items[0].qty, 20) + self.assertEqual(plan.sub_assembly_items[1].qty, 50) + + from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_items_for_material_requests, + ) + + mr_items = get_items_for_material_requests(plan.as_dict()) + + self.assertEqual(mr_items[0].get("quantity"), 80) + self.assertEqual(mr_items[1].get("quantity"), 70) + def create_production_plan(**args): """ From dedb19e3e9f0df09a48b56ec2ed0973293f103ee Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 11 Apr 2025 19:30:35 +0530 Subject: [PATCH 10/49] fix: test cases (cherry picked from commit a7394329cab73a8203ab3444fd7d44f4056e896c) --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 2d5dfbf32fe..c285e34c7e6 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1776,7 +1776,7 @@ def get_sub_assembly_items( frappe._dict( { "actual_qty": bin_details[d.item_code][0].get("actual_qty", 0) - if bin_details + if bin_details[d.item_code] else 0, "parent_item_code": parent_item_code, "description": d.description, From 13d3b27a1fe91e8b017968e064d2ab123ef62ba5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Apr 2025 11:09:45 +0530 Subject: [PATCH 11/49] fix: test cases error (cherry picked from commit 8df18762a9ded000fd82bef20789622ed60cb9d4) --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index c285e34c7e6..1e27e8d59de 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1776,7 +1776,7 @@ def get_sub_assembly_items( frappe._dict( { "actual_qty": bin_details[d.item_code][0].get("actual_qty", 0) - if bin_details[d.item_code] + if bin_details.get(d.item_code) else 0, "parent_item_code": parent_item_code, "description": d.description, From 924e9b94b641f8fb816f3a45868bf8381570a595 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Apr 2025 13:28:06 +0530 Subject: [PATCH 12/49] fix: import error --- erpnext/manufacturing/doctype/production_plan/production_plan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 1e27e8d59de..937af0fac54 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -22,6 +22,7 @@ from frappe.utils import ( ) from frappe.utils.csvutils import build_csv_response from pypika.terms import ExistsCriterion +from collections import defaultdict from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children from erpnext.manufacturing.doctype.bom.bom import validate_bom_no From c58800a92949975e817d38630bdfee32cd78c5e6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Apr 2025 13:33:48 +0530 Subject: [PATCH 13/49] fix: linter --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 937af0fac54..2e89a191f66 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -4,6 +4,7 @@ import copy import json +from collections import defaultdict import frappe from frappe import _, msgprint @@ -22,7 +23,6 @@ from frappe.utils import ( ) from frappe.utils.csvutils import build_csv_response from pypika.terms import ExistsCriterion -from collections import defaultdict from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children from erpnext.manufacturing.doctype.bom.bom import validate_bom_no From 5cb8e5dfbd7e81f7526aac8e1eb40ab1d898e626 Mon Sep 17 00:00:00 2001 From: Himanshu Shivhare Date: Thu, 10 Apr 2025 23:18:43 +0530 Subject: [PATCH 14/49] chore: Fix typo "Item Wise Tax Detail " (cherry picked from commit 9624d56abd3e5a7f1e2c4b96a1c26f2048e3f31a) --- .../purchase_taxes_and_charges/purchase_taxes_and_charges.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index e82649a25ab..10130ec344f 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -196,7 +196,7 @@ "fieldname": "item_wise_tax_detail", "fieldtype": "Code", "hidden": 1, - "label": "Item Wise Tax Detail ", + "label": "Item Wise Tax Detail", "oldfieldname": "item_wise_tax_detail", "oldfieldtype": "Small Text", "print_hide": 1, From 1dc98124dc0840e4ef2bc44429d300f7ba544dd7 Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:15:50 +0200 Subject: [PATCH 15/49] fix: Modify .json from desk to change `modified` (cherry picked from commit be556167b1249f4a5f9ffe196528bf6773b22981) # Conflicts: # erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json --- .../purchase_taxes_and_charges.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 10130ec344f..0d92a538b3d 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -235,17 +235,27 @@ "read_only": 1 } ], + "grid_page_length": 50, "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2024-04-08 19:51:36.678551", +======= + "modified": "2025-04-15 13:14:48.936047", +>>>>>>> be556167b1 (fix: Modify .json from desk to change `modified`) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", "naming_rule": "Random", "owner": "Administrator", "permissions": [], +<<<<<<< HEAD "sort_field": "modified", +======= + "row_format": "Dynamic", + "sort_field": "creation", +>>>>>>> be556167b1 (fix: Modify .json from desk to change `modified`) "sort_order": "DESC", "track_changes": 1 } From 99735e0af489a2c4faec57436eea0c2ed0cc6680 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 16 Apr 2025 10:18:29 +0530 Subject: [PATCH 16/49] revert: disable customer if creating from opportunity (cherry picked from commit fc16199a49141315b5e1e78a178a49f7d8cc6b09) --- erpnext/selling/doctype/quotation/quotation.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index b9d46e0b84b..2e88ba9e482 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -70,7 +70,8 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. onload(doc, dt, dn) { super.onload(doc, dt, dn); - this.frm.trigger("disable_customer_if_creating_from_opportunity"); + // TODO: think of better way to do this + // this.frm.trigger("disable_customer_if_creating_from_opportunity"); } party_name() { var me = this; From be154a469fb2b0955f43fffc2365be6b20a43b65 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Apr 2025 12:36:59 +0530 Subject: [PATCH 17/49] fix: consider per_ordered instead of per_billed when creating PO from MR (cherry picked from commit 5a524854de5d68ccf67313530dfb8faeab2d2128) # Conflicts: # erpnext/stock/doctype/material_request/material_request.js --- .../material_request/material_request.js | 19 +++++++++++++++++-- .../material_request/material_request.py | 4 ++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 83b63e225b3..86a4333eb65 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -107,6 +107,7 @@ frappe.ui.form.on("Material Request", { if (flt(frm.doc.per_received, precision) < 100) { frm.add_custom_button(__("Stop"), () => frm.events.update_status(frm, "Stopped")); +<<<<<<< HEAD if (frm.doc.material_request_type === "Purchase") { frm.add_custom_button( @@ -115,6 +116,8 @@ frappe.ui.form.on("Material Request", { __("Create") ); } +======= +>>>>>>> 5a524854de (fix: consider per_ordered instead of per_billed when creating PO from MR) } if (flt(frm.doc.per_ordered, precision) < 100) { @@ -158,14 +161,18 @@ frappe.ui.form.on("Material Request", { } if (frm.doc.material_request_type === "Purchase") { + frm.add_custom_button( + __("Purchase Order"), + () => frm.events.make_purchase_order(frm), + __("Create") + ); + frm.add_custom_button( __("Request for Quotation"), () => frm.events.make_request_for_quotation(frm), __("Create") ); - } - if (frm.doc.material_request_type === "Purchase") { frm.add_custom_button( __("Supplier Quotation"), () => frm.events.make_supplier_quotation(frm), @@ -181,6 +188,14 @@ frappe.ui.form.on("Material Request", { ); } + if (frm.doc.material_request_type === "Subcontracting") { + frm.add_custom_button( + __("Subcontracted Purchase Order"), + () => frm.events.make_purchase_order(frm), + __("Create") + ); + } + frm.page.set_inner_btn_group_as_primary(__("Create")); } } diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 59634cb9f7c..a5f1467cced 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -379,7 +379,7 @@ def set_missing_values(source, target_doc): def update_item(obj, target, source_parent): target.conversion_factor = obj.conversion_factor - qty = obj.received_qty or obj.ordered_qty + qty = obj.ordered_qty or obj.received_qty target.qty = flt(flt(obj.stock_qty) - flt(qty)) / target.conversion_factor target.stock_qty = target.qty * target.conversion_factor if getdate(target.schedule_date) < getdate(nowdate()): @@ -432,7 +432,7 @@ def make_purchase_order(source_name, target_doc=None, args=None): filtered_items = args.get("filtered_children", []) child_filter = d.name in filtered_items if filtered_items else True - qty = d.received_qty or d.ordered_qty + qty = d.ordered_qty or d.received_qty return qty < d.stock_qty and child_filter From 20aba541c4218fd4847ec4feb9983aba6ec73d0f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 16 Apr 2025 20:32:05 +0530 Subject: [PATCH 18/49] fix: cherry pick --- .../doctype/material_request/material_request.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 86a4333eb65..5f46ef968b0 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -107,17 +107,6 @@ frappe.ui.form.on("Material Request", { if (flt(frm.doc.per_received, precision) < 100) { frm.add_custom_button(__("Stop"), () => frm.events.update_status(frm, "Stopped")); -<<<<<<< HEAD - - if (frm.doc.material_request_type === "Purchase") { - frm.add_custom_button( - __("Purchase Order"), - () => frm.events.make_purchase_order(frm), - __("Create") - ); - } -======= ->>>>>>> 5a524854de (fix: consider per_ordered instead of per_billed when creating PO from MR) } if (flt(frm.doc.per_ordered, precision) < 100) { From f07c3d91240d6bf7f0922cdd298ec38bcc54c3f4 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Thu, 17 Apr 2025 00:12:43 +0530 Subject: [PATCH 19/49] fix: add group by after user permission condition (cherry picked from commit 756d496235af3223e64ae33c51fd5b63d91856da) --- erpnext/accounts/report/financial_statements.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 67154455f95..0d1e185382d 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -519,9 +519,6 @@ def get_accounting_entries( .where(gl_entry.company == filters.company) ) - if group_by_account: - query = query.groupby(gl_entry.account) - ignore_is_opening = frappe.db.get_single_value( "Accounts Settings", "ignore_is_opening_check_for_reporting" ) @@ -551,6 +548,9 @@ def get_accounting_entries( if match_conditions: query += "and" + match_conditions + if group_by_account: + query += " GROUP BY `account`" + return frappe.db.sql(query, params, as_dict=True) From ad177e08b80238b2ca3b6f93ed6a8261dbe7bc3f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 17 Apr 2025 13:40:05 +0200 Subject: [PATCH 20/49] fix: create default warehouse (backport #47125) (#47131) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> fix: create default warehouse (#47125) --- erpnext/setup/doctype/company/company.py | 44 ++++++++++++++---------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 8ae843abc24..e5f26073a33 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -282,6 +282,7 @@ class Company(NestedSet): frappe.clear_cache() def create_default_warehouses(self): + parent_warehouse = None for wh_detail in [ {"warehouse_name": _("All Warehouses"), "is_group": 1}, {"warehouse_name": _("Stores"), "is_group": 0}, @@ -289,24 +290,31 @@ class Company(NestedSet): {"warehouse_name": _("Finished Goods"), "is_group": 0}, {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}, ]: - if not frappe.db.exists("Warehouse", "{} - {}".format(wh_detail["warehouse_name"], self.abbr)): - warehouse = frappe.get_doc( - { - "doctype": "Warehouse", - "warehouse_name": wh_detail["warehouse_name"], - "is_group": wh_detail["is_group"], - "company": self.name, - "parent_warehouse": "{} - {}".format(_("All Warehouses"), self.abbr) - if not wh_detail["is_group"] - else "", - "warehouse_type": wh_detail["warehouse_type"] - if "warehouse_type" in wh_detail - else None, - } - ) - warehouse.flags.ignore_permissions = True - warehouse.flags.ignore_mandatory = True - warehouse.insert() + if frappe.db.exists( + "Warehouse", + { + "warehouse_name": wh_detail["warehouse_name"], + "company": self.name, + }, + ): + continue + + warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": wh_detail["warehouse_name"], + "is_group": wh_detail["is_group"], + "company": self.name, + "parent_warehouse": parent_warehouse, + "warehouse_type": wh_detail.get("warehouse_type"), + } + ) + warehouse.flags.ignore_permissions = True + warehouse.flags.ignore_mandatory = True + warehouse.insert() + + if wh_detail["is_group"]: + parent_warehouse = warehouse.name def create_default_accounts(self): from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts From 451b1a19a85bdd19b2c8fd8fcda057d072f2b154 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 17 Apr 2025 14:18:00 +0200 Subject: [PATCH 21/49] chore: migrate pre-commit config (backport #47132) (#47134) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7375ca14727..13cbf66a5af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ exclude: 'node_modules|.git' -default_stages: [commit] +default_stages: [pre-commit] fail_fast: false From 846b24ba52c016e5a41b911fdece42029db9119e Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Sat, 19 Apr 2025 11:16:48 +0530 Subject: [PATCH 22/49] fix: respect mapped accounting dimensions (cherry picked from commit 7dbe27da198e142caa1679e0ca35c1a7b553e624) --- .../public/js/utils/dimension_tree_filter.js | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 36c0f1b51ae..68bf11de5d8 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -77,35 +77,34 @@ erpnext.accounts.dimensions = { }, update_dimension(frm, doctype) { - if (this.accounting_dimensions) { - this.accounting_dimensions.forEach((dimension) => { - if (frm.is_new()) { - if ( - frm.doc.company && - Object.keys(this.default_dimensions || {}).length > 0 && - this.default_dimensions[frm.doc.company] - ) { - let default_dimension = - this.default_dimensions[frm.doc.company][dimension["fieldname"]]; + if ( + !this.accounting_dimensions || + !frm.is_new() || + !frm.doc.company || + !this.default_dimensions?.[frm.doc.company] + ) + return; - if (default_dimension) { - if (frappe.meta.has_field(doctype, dimension["fieldname"])) { - frm.set_value(dimension["fieldname"], default_dimension); - } - - $.each(frm.doc.items || frm.doc.accounts || [], function (i, row) { - frappe.model.set_value( - row.doctype, - row.name, - dimension["fieldname"], - default_dimension - ); - }); - } - } - } - }); + // don't set default dimensions if any of the dimension is already set due to mapping + if (frm.doc.__onload?.load_after_mapping) { + for (const dimension of this.accounting_dimensions) { + if (frm.doc[dimension["fieldname"]]) return; + } } + + this.accounting_dimensions.forEach((dimension) => { + const default_dimension = this.default_dimensions[frm.doc.company][dimension["fieldname"]]; + + if (!default_dimension) return; + + if (frappe.meta.has_field(doctype, dimension["fieldname"])) { + frm.set_value(dimension["fieldname"], default_dimension); + } + + (frm.doc.items || frm.doc.accounts || []).forEach((row) => { + frappe.model.set_value(row.doctype, row.name, dimension["fieldname"], default_dimension); + }); + }); }, copy_dimension_from_first_row(frm, cdt, cdn, fieldname) { From 7adba1f0f3b346438ae0b5b705437dfb3c0d61d6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 11:56:54 +0530 Subject: [PATCH 23/49] fix: pos disable customer selection at payment (backport #47169) (#47170) fix: pos disable customer selection at payment (#47169) (cherry picked from commit f52cbf6165b2ce9c4b15eda563abb16575282798) Co-authored-by: Diptanil Saha --- .../page/point_of_sale/pos_item_cart.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index dee052912ff..d67ad067c26 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -188,6 +188,7 @@ erpnext.PointOfSale.ItemCart = class { await me.events.checkout(); me.toggle_checkout_btn(false); + me.disable_customer_selection(); me.allow_discount_change && me.$add_discount_elem.removeClass("d-none"); }); @@ -195,6 +196,7 @@ erpnext.PointOfSale.ItemCart = class { this.$totals_section.on("click", ".edit-cart-btn", () => { this.events.edit_cart(); this.toggle_checkout_btn(true); + me.enable_customer_selection(); }); this.$component.on("click", ".add-discount-wrapper", () => { @@ -698,6 +700,25 @@ erpnext.PointOfSale.ItemCart = class { } } + disable_customer_selection() { + this.$customer_section.find(".reset-customer-btn").css("visibility", "hidden"); + this.$customer_section.off("click", ".customer-display"); + this.$customer_section.off("click", ".reset-customer-btn"); + } + + enable_customer_selection() { + this.$customer_section.find(".reset-customer-btn").css("visibility", "visible"); + this.$customer_section.on("click", ".customer-display", (e) => { + if ($(e.target).closest(".reset-customer-btn").length) return; + + const show = this.$cart_container.is(":visible"); + this.toggle_customer_info(show); + }); + this.$customer_section.on("click", ".reset-customer-btn", () => { + this.reset_customer_selector(); + }); + } + highlight_checkout_btn(toggle) { if (toggle) { this.$add_discount_elem.css("display", "flex"); From 390780d8717ac1200574d8f56b426e7d13c58896 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 14:26:47 +0530 Subject: [PATCH 24/49] fix: update country wise fiscal year (backport #47141) (#47176) fix: update country wise fiscal year (#47141) (cherry picked from commit cb2ad4acdbba505bf93e49b35a8aa9bfb1cde939) Co-authored-by: Diptanil Saha --- erpnext/public/js/setup_wizard.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index 3879b38aa13..2ced08ed5d2 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -240,13 +240,16 @@ erpnext.setup.fiscal_years = { Afghanistan: ["12-21", "12-20"], Australia: ["07-01", "06-30"], Bangladesh: ["07-01", "06-30"], - Canada: ["04-01", "03-31"], "Costa Rica": ["10-01", "09-30"], Egypt: ["07-01", "06-30"], + Ethiopia: ["07-08", "07-07"], "Hong Kong": ["04-01", "03-31"], India: ["04-01", "03-31"], Iran: ["06-23", "06-22"], + Kenya: ["07-01", "06-30"], + Malaysia: ["07-01", "06-30"], Myanmar: ["04-01", "03-31"], + Nepal: ["07-16", "07-15"], "New Zealand": ["04-01", "03-31"], Pakistan: ["07-01", "06-30"], Singapore: ["04-01", "03-31"], From b8a7f6dac1bff8f54868ae1251b9beb02f6f45e8 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 14 Apr 2025 19:06:22 +0530 Subject: [PATCH 25/49] fix: correct error message in validate_internal_transfer_qty (cherry picked from commit 5063f1174e5d6013ba14afedf6849b98ce0c65f8) --- erpnext/controllers/stock_controller.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 10799744d41..d07723f02df 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1153,6 +1153,12 @@ class StockController(AccountsController): if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]: return + self.__inter_company_reference = ( + self.get("inter_company_reference") + if self.doctype == "Purchase Invoice" + else self.get("inter_company_invoice_reference") + ) + item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty() if not item_wise_transfer_qty: return @@ -1182,15 +1188,11 @@ class StockController(AccountsController): bold(key[1]), bold(flt(transferred_qty, precision)), bold(parent_doctype), - get_link_to_form(parent_doctype, self.get("inter_company_reference")), + get_link_to_form(parent_doctype, self.__inter_company_reference), ) ) def get_item_wise_inter_transfer_qty(self): - reference_field = "inter_company_reference" - if self.doctype == "Purchase Invoice": - reference_field = "inter_company_invoice_reference" - parent_doctype = { "Purchase Receipt": "Delivery Note", "Purchase Invoice": "Sales Invoice", @@ -1210,7 +1212,7 @@ class StockController(AccountsController): child_tab.item_code, child_tab.qty, ) - .where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1)) + .where((parent_tab.name == self.__inter_company_reference) & (parent_tab.docstatus == 1)) ) data = query.run(as_dict=True) From f53a45cfefd6896c99c2f8d794021fc07f505ea4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 21 Apr 2025 16:30:20 +0530 Subject: [PATCH 26/49] chore: resolve conflict --- .../purchase_taxes_and_charges.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 0d92a538b3d..008d63ae76f 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -239,23 +239,14 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2024-04-08 19:51:36.678551", -======= "modified": "2025-04-15 13:14:48.936047", ->>>>>>> be556167b1 (fix: Modify .json from desk to change `modified`) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", "naming_rule": "Random", "owner": "Administrator", "permissions": [], -<<<<<<< HEAD "sort_field": "modified", -======= - "row_format": "Dynamic", - "sort_field": "creation", ->>>>>>> be556167b1 (fix: Modify .json from desk to change `modified`) "sort_order": "DESC", "track_changes": 1 } From 5535eb481747ebc54f917ae1c5097e3ab810e69e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 18 Apr 2025 16:09:35 +0530 Subject: [PATCH 27/49] fix: provision to recalculate the qty in the Bin (cherry picked from commit 36081413d807e94b70b6a89483b8439e1b1851bb) --- erpnext/stock/doctype/bin/bin.js | 17 ++++++- erpnext/stock/doctype/bin/bin.py | 86 +++++++++++++++++++++----------- 2 files changed, 74 insertions(+), 29 deletions(-) diff --git a/erpnext/stock/doctype/bin/bin.js b/erpnext/stock/doctype/bin/bin.js index 02ff8b62396..c725b691db4 100644 --- a/erpnext/stock/doctype/bin/bin.js +++ b/erpnext/stock/doctype/bin/bin.js @@ -2,5 +2,20 @@ // For license information, please see license.txt frappe.ui.form.on("Bin", { - refresh: function (frm) {}, + refresh(frm) { + frm.trigger("recalculate_bin_quantity"); + }, + + recalculate_bin_quantity(frm) { + frm.add_custom_button(__("Recalculate Bin Qty"), () => { + frappe.call({ + method: "recalculate_qty", + freeze: true, + doc: frm.doc, + callback: function (r) { + frappe.show_alert(__("Bin Qty Recalculated"), 2); + }, + }); + }); + }, }); diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index d3de1897633..338fd863ffc 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -35,6 +35,28 @@ class Bin(Document): warehouse: DF.Link # end: auto-generated types + @frappe.whitelist() + def recalculate_qty(self): + from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production + from erpnext.stock.stock_balance import ( + get_indented_qty, + get_ordered_qty, + get_planned_qty, + get_reserved_qty, + ) + + self.actual_qty = get_actual_qty(self.item_code, self.warehouse) + self.planned_qty = get_planned_qty(self.item_code, self.warehouse) + self.indented_qty = get_indented_qty(self.item_code, self.warehouse) + self.ordered_qty = get_ordered_qty(self.item_code, self.warehouse) + self.reserved_qty = get_reserved_qty(self.item_code, self.warehouse) + self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse) + + self.update_reserved_qty_for_sub_contracting(update_qty=False) + self.update_reserved_qty_for_production_plan(skip_project_qty_update=True, update_qty=False) + self.set_projected_qty() + self.save() + def before_save(self): if self.get("__islocal") or not self.stock_uom: self.stock_uom = frappe.get_cached_value("Item", self.item_code, "stock_uom") @@ -52,7 +74,7 @@ class Bin(Document): - flt(self.reserved_qty_for_production_plan) ) - def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False): + def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False, update_qty=True): """Update qty reserved for production from Production Plan tables in open production plan""" from erpnext.manufacturing.doctype.production_plan.production_plan import ( @@ -68,11 +90,12 @@ class Bin(Document): self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan) - self.db_set( - "reserved_qty_for_production_plan", - flt(self.reserved_qty_for_production_plan), - update_modified=True, - ) + if update_qty: + self.db_set( + "reserved_qty_for_production_plan", + flt(self.reserved_qty_for_production_plan), + update_modified=True, + ) if not skip_project_qty_update: self.set_projected_qty() @@ -115,7 +138,9 @@ class Bin(Document): self.set_projected_qty() self.db_set("projected_qty", self.projected_qty, update_modified=True) - def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"): + def update_reserved_qty_for_sub_contracting( + self, subcontract_doctype="Subcontracting Order", update_qty=True + ): # reserved qty subcontract_order = frappe.qb.DocType(subcontract_doctype) @@ -191,9 +216,11 @@ class Bin(Document): else: reserved_qty_for_sub_contract = 0 - self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract, update_modified=True) - self.set_projected_qty() - self.db_set("projected_qty", self.projected_qty, update_modified=True) + self.reserved_qty_for_sub_contract = reserved_qty_for_sub_contract + if update_qty: + self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract, update_modified=True) + self.set_projected_qty() + self.db_set("projected_qty", self.projected_qty, update_modified=True) def update_reserved_stock(self): """Update `Reserved Stock` on change in Reserved Qty of Stock Reservation Entry""" @@ -235,27 +262,10 @@ def update_qty(bin_name, args): bin_details = get_bin_details(bin_name) # actual qty is already updated by processing current voucher actual_qty = bin_details.actual_qty or 0.0 - sle = frappe.qb.DocType("Stock Ledger Entry") # actual qty is not up to date in case of backdated transaction if future_sle_exists(args, allow_force_reposting=False): - last_sle_qty = ( - frappe.qb.from_(sle) - .select(sle.qty_after_transaction) - .where( - (sle.item_code == args.get("item_code")) - & (sle.warehouse == args.get("warehouse")) - & (sle.is_cancelled == 0) - ) - .orderby(sle.posting_datetime, order=Order.desc) - .orderby(sle.creation, order=Order.desc) - .limit(1) - .run() - ) - - actual_qty = 0.0 - if last_sle_qty: - actual_qty = last_sle_qty[0][0] + actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse")) ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty")) reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty")) @@ -287,3 +297,23 @@ def update_qty(bin_name, args): }, update_modified=True, ) + + +def get_actual_qty(item_code, warehouse): + sle = frappe.qb.DocType("Stock Ledger Entry") + + last_sle_qty = ( + frappe.qb.from_(sle) + .select(sle.qty_after_transaction) + .where((sle.item_code == item_code) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0)) + .orderby(sle.posting_datetime, order=Order.desc) + .orderby(sle.creation, order=Order.desc) + .limit(1) + .run() + ) + + actual_qty = 0.0 + if last_sle_qty: + actual_qty = last_sle_qty[0][0] + + return actual_qty From 3d4f3e1be7a6fac4fe6dfef3effca13443851390 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Apr 2025 21:24:58 +0530 Subject: [PATCH 28/49] fix: disbaled UOM showing in the list (cherry picked from commit 374582505242650bd71e765f378f9a7efc680b69) --- erpnext/controllers/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 670d46453f9..50d7ad641de 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -921,7 +921,7 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters): return frappe.get_all( "UOM", - filters={"name": ["like", f"%{txt}%"]}, + filters={"name": ["like", f"%{txt}%"], "enabled": 1}, fields=["name"], limit_start=start, limit_page_length=page_len, From 3b349f44b1eb7256d14e2895bfa26ab81403a46a Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 17 Apr 2025 15:39:50 +0530 Subject: [PATCH 29/49] fix: `TypeError` in group field filter in supplier ledger summary (cherry picked from commit 872e94a316ac10ee2c8d47c5a052d98cc23da3a1) --- .../customer_ledger_summary.py | 2 +- .../test_supplier_ledger_summary.py | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index ad05d770314..3dfcd4463d9 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -100,7 +100,7 @@ class PartyLedgerSummaryReport: conditions.append(doctype.territory.isin(self.filters.territory)) if self.filters.get(group_field): - conditions.append(doctype.get(group_field).isin(self.filters.get(group_field))) + conditions.append(doctype[group_field].isin(self.filters.get(group_field))) if self.filters.payment_terms_template: conditions.append(doctype.payment_terms == self.filters.payment_terms_template) diff --git a/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py b/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py index ea95772af4d..4686ccd094b 100644 --- a/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py +++ b/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py @@ -59,3 +59,33 @@ class TestSupplierLedgerSummary(FrappeTestCase, AccountsTestMixin): for field in expected: with self.subTest(field=field): self.assertEqual(report_output[0].get(field), expected.get(field)) + + def test_supplier_ledger_summary_with_filters(self): + self.create_purchase_invoice() + + supplier_group = frappe.db.get_value("Supplier", self.supplier, "supplier_group") + + filters = { + "company": self.company, + "from_date": today(), + "to_date": today(), + "supplier_group": supplier_group, + } + + expected = { + "party": "_Test Supplier", + "party_name": "_Test Supplier", + "opening_balance": 0, + "invoiced_amount": 300.0, + "paid_amount": 0, + "return_amount": 0, + "closing_balance": 300.0, + "currency": "INR", + "supplier_name": "_Test Supplier", + } + + report_output = execute(filters)[1] + self.assertEqual(len(report_output), 1) + for field in expected: + with self.subTest(field=field): + self.assertEqual(report_output[0].get(field), expected.get(field)) From 2f1f2291441b10e97491c725ce3881c56e857df2 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 22 Apr 2025 08:07:09 +0530 Subject: [PATCH 30/49] fix: expense account in stock entry (cherry picked from commit 75874b4986a04dae751b8034db3e6396e78de078) --- erpnext/stock/doctype/material_request/material_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index a5f1467cced..54b3e17641e 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -721,6 +721,7 @@ def make_stock_entry(source_name, target_doc=None): "uom": "stock_uom", "job_card_item": "job_card_item", }, + "field_no_map": ["expense_account"], "postprocess": update_item, "condition": lambda doc: ( flt(doc.ordered_qty, doc.precision("ordered_qty")) From 9184c40371ff1449791c25907fa3967a654169ba Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 17 Apr 2025 20:23:48 +0530 Subject: [PATCH 31/49] fix: rate based on posting date in Tax Withholding Report (cherry picked from commit a32a79e90a97a8a5814c9cc7ece26dc598708f3c) --- .../tax_withholding_details.py | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 42c6c82b19f..ee52da291b1 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.utils import getdate def execute(filters=None): @@ -33,6 +34,7 @@ def execute(filters=None): def validate_filters(filters): """Validate if dates are properly set""" + filters = frappe._dict(filters or {}) if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) @@ -68,7 +70,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ if not tax_withholding_category: tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") - rate = tax_rate_map.get(tax_withholding_category) + rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date) if net_total_map.get((voucher_type, name)): if voucher_type == "Journal Entry" and tax_amount and rate: # back calcalute total amount from rate and tax_amount @@ -435,12 +437,22 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): def get_tax_rate_map(filters): rate_map = frappe.get_all( "Tax Withholding Rate", - filters={ - "from_date": ("<=", filters.get("from_date")), - "to_date": (">=", filters.get("to_date")), - }, - fields=["parent", "tax_withholding_rate"], - as_list=1, + filters={"from_date": ("<=", filters.to_date), "to_date": (">=", filters.from_date)}, + fields=["parent", "tax_withholding_rate", "from_date", "to_date"], ) - return frappe._dict(rate_map) + rate_list = frappe._dict() + + for rate in rate_map: + rate_list.setdefault(rate.parent, []).append(frappe._dict(rate)) + + return rate_list + + +def get_tax_withholding_rates(tax_withholding, posting_date): + # returns the row that matches with the fiscal year from posting date + for rate in tax_withholding: + if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date): + return rate.tax_withholding_rate + + return 0 From 1f8fce253dd2415dec5b4f1994967a0e539d60df Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 18 Apr 2025 13:25:42 +0530 Subject: [PATCH 32/49] chore: added test case for date period in multiple tax withholding rules (cherry picked from commit 515fe340a86663553bf4cdc7817b133ee4590037) # Conflicts: # erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py --- .../test_tax_withholding_details.py | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py index 7515616b0b8..6eff81e7f42 100644 --- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import today +from frappe.utils import add_to_date, today from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice @@ -60,6 +60,56 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase): ] self.check_expected_values(result, expected_values) + def test_date_filters_in_multiple_tax_withholding_rules(self): + create_tax_category("TDS - 3", rate=10, account="TDS - _TC", cumulative_threshold=1) + # insert new rate in same fiscal year + fiscal_year = get_fiscal_year(today(), company="_Test Company") + mid_year = add_to_date(fiscal_year[1], months=6) + tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3") + tds_doc.rates[0].to_date = mid_year + tds_doc.append( + "rates", + { + "tax_withholding_rate": 20, + "from_date": add_to_date(mid_year, days=1), + "to_date": fiscal_year[2], + "single_threshold": 1, + "cumulative_threshold": 1, + }, + ) + + tds_doc.save() + + inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True) + inv_1.apply_tds = 1 + inv_1.tax_withholding_category = "TDS - 3" + inv_1.submit() + + inv_2 = make_purchase_invoice( + rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True + ) + inv_2.set_posting_time = 1 + + inv_1.apply_tds = 1 + inv_2.tax_withholding_category = "TDS - 3" + inv_2.save() + inv_2.submit() + + result = execute( + frappe._dict( + company="_Test Company", + party_type="Supplier", + from_date=fiscal_year[1], + to_date=fiscal_year[2], + ) + )[1] + + expected_values = [ + [inv_1.name, "TDS - 3", 10.0, 5000, 500, 4500], + [inv_2.name, "TDS - 3", 20.0, 5000, 1000, 4000], + ] + self.check_expected_values(result, expected_values) + def check_expected_values(self, result, expected_values): for i in range(len(result)): voucher = frappe._dict(result[i]) From d7556069e440e2412a7026d918a9e86955645d2c Mon Sep 17 00:00:00 2001 From: Soham Kulkarni <77533095+sokumon@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:19:41 +0530 Subject: [PATCH 33/49] Merge pull request #47175 from sokumon/purchase-receipt-quick-list fix: add grand_total to show correct status in quick list widget (cherry picked from commit 68ca4a77c9034359b21974199dcb620b5e42d4ee) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index e295127e6fc..e95a1a2e9f8 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js @@ -17,7 +17,7 @@ frappe.listview_settings["Purchase Receipt"] = { return [__("Closed"), "green", "status,=,Closed"]; } else if (flt(doc.per_returned, 2) === 100) { return [__("Return Issued"), "grey", "per_returned,=,100|docstatus,=,1"]; - } else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) == 0) { + } else if (flt(doc.grand_total || doc.base_grand_total) !== 0 && flt(doc.per_billed, 2) == 0) { return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"]; } else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) { return [__("Partly Billed"), "yellow", "per_billed,<,100|docstatus,=,1"]; From 05d4c1e6cad695278797976d289c25fb74d58307 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 21 Apr 2025 17:38:34 +0530 Subject: [PATCH 34/49] fix: set default company address in Sales Doctype on change of company (cherry picked from commit a31075692c02b30f9442d29365247abdd0f2ba64) --- .../doctype/sales_invoice/sales_invoice.js | 18 ------------- erpnext/public/js/utils/sales_common.js | 27 +++++++++++++++++++ .../doctype/sales_order/sales_order.js | 21 --------------- 3 files changed, 27 insertions(+), 39 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 792e1ddbdad..540f3778232 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -782,24 +782,6 @@ frappe.ui.form.on("Sales Invoice", { }; }; }, - // When multiple companies are set up. in case company name is changed set default company address - company: function (frm) { - if (frm.doc.company) { - frappe.call({ - method: "erpnext.setup.doctype.company.company.get_default_company_address", - args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" }, - debounce: 2000, - callback: function (r) { - if (r.message) { - frm.set_value("company_address", r.message); - } else { - frm.set_value("company_address", ""); - } - }, - }); - } - }, - onload: function (frm) { frm.redemption_conversion_factor = null; }, diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index bbb2a88e629..ffff536f068 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -111,6 +111,33 @@ erpnext.sales_common = { this.toggle_editable_price_list_rate(); } + company() { + super.company(); + this.set_default_company_address(); + } + + set_default_company_address() { + if (!frappe.meta.has_field(this.frm.doc.doctype, "company_address")) return; + var me = this; + if (this.frm.doc.company) { + frappe.call({ + method: "erpnext.setup.doctype.company.company.get_default_company_address", + args: { + name: this.frm.doc.company, + existing_address: this.frm.doc.company_address || "", + }, + debounce: 2000, + callback: function (r) { + if (r.message) { + me.frm.set_value("company_address", r.message); + } else { + me.frm.set_value("company_address", ""); + } + }, + }); + } + } + customer() { var me = this; erpnext.utils.get_party_details(this.frm, null, null, function () { diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index de4053458e4..400acd3c6f1 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -164,27 +164,6 @@ frappe.ui.form.on("Sales Order", { ); }, - // When multiple companies are set up. in case company name is changed set default company address - company: function (frm) { - if (frm.doc.company) { - frappe.call({ - method: "erpnext.setup.doctype.company.company.get_default_company_address", - args: { - name: frm.doc.company, - existing_address: frm.doc.company_address || "", - }, - debounce: 2000, - callback: function (r) { - if (r.message) { - frm.set_value("company_address", r.message); - } else { - frm.set_value("company_address", ""); - } - }, - }); - } - }, - onload: function (frm) { if (!frm.doc.transaction_date) { frm.set_value("transaction_date", frappe.datetime.get_today()); From cb2b9563e087d1bbd8718d998e9c00941bed4600 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 21 Apr 2025 20:23:14 +0530 Subject: [PATCH 35/49] feat: add button to show request for comparison report directly from RFQ (cherry picked from commit b4aa88b59b112c07e89951687f201b452ede6506) --- .../request_for_quotation.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index d7c2c3f24b1..2c0538b1046 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -155,6 +155,28 @@ frappe.ui.form.on("Request for Quotation", { frm.page.set_inner_btn_group_as_primary(__("Create")); } + + frm.add_custom_button( + __("Supplier Quotation Comparison"), + function () { + frm.trigger("show_supplier_quotation_comparison"); + }, + __("View") + ); + }, + + show_supplier_quotation_comparison(frm) { + const today = new Date(); + const oneMonthAgo = new Date(today); + oneMonthAgo.setMonth(today.getMonth() - 1); + + frappe.route_options = { + company: frm.doc.company, + from_date: moment(oneMonthAgo).format("YYYY-MM-DD"), + to_date: moment(today).format("YYYY-MM-DD"), + request_for_quotation: frm.doc.name, + }; + frappe.set_route("query-report", "Supplier Quotation Comparison"); }, make_supplier_quotation: function (frm) { From 9655bfa1999b4ecd97a1a4dcd7b14b91b151b301 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 21 Apr 2025 20:25:31 +0530 Subject: [PATCH 36/49] fix: show button only when RFQ is submitted (cherry picked from commit ef57d2b3289c3d97aba11b9aac00f038d06de295) --- .../request_for_quotation.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 2c0538b1046..96597bd9753 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -154,15 +154,15 @@ frappe.ui.form.on("Request for Quotation", { ); frm.page.set_inner_btn_group_as_primary(__("Create")); - } - frm.add_custom_button( - __("Supplier Quotation Comparison"), - function () { - frm.trigger("show_supplier_quotation_comparison"); - }, - __("View") - ); + frm.add_custom_button( + __("Supplier Quotation Comparison"), + function () { + frm.trigger("show_supplier_quotation_comparison"); + }, + __("View") + ); + } }, show_supplier_quotation_comparison(frm) { From 5dc63f97a11f2f6f129bc4c7a331f03acacd14be Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 21 Apr 2025 12:11:13 +0530 Subject: [PATCH 37/49] fix: set correct paid/receive amount if doc currency is different from party account currency (cherry picked from commit 96125218943c2740b3688a6d0b99b67bff7d7086) --- .../doctype/payment_entry/payment_entry.py | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 77eeba5cf3a..00c9a337499 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -3300,26 +3300,25 @@ def set_paid_amount_and_received_amount( if party_account_currency == bank.account_currency: paid_amount = received_amount = abs(outstanding_amount) else: - company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency") - if payment_type == "Receive": - paid_amount = abs(outstanding_amount) - if bank_amount: - received_amount = bank_amount - else: - if bank and company_currency != bank.account_currency: - received_amount = paid_amount / doc.get("conversion_rate", 1) - else: - received_amount = paid_amount * doc.get("conversion_rate", 1) + # settings if it is for receive + paid_amount = abs(outstanding_amount) + if bank_amount: + received_amount = bank_amount else: - received_amount = abs(outstanding_amount) - if bank_amount: - paid_amount = bank_amount + company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency") + if bank and company_currency != bank.account_currency: + # doc currency can be different from bank currency + posting_date = doc.get("posting_date") or doc.get("transaction_date") + conversion_rate = get_exchange_rate( + bank.account_currency, party_account_currency, posting_date + ) + received_amount = paid_amount / conversion_rate else: - if bank and company_currency != bank.account_currency: - paid_amount = received_amount / doc.get("conversion_rate", 1) - else: - # if party account currency and bank currency is different then populate paid amount as well - paid_amount = received_amount * doc.get("conversion_rate", 1) + received_amount = paid_amount * doc.get("conversion_rate", 1) + + # if payment type is pay, then paid amount and received amount are swapped + if payment_type == "Pay": + paid_amount, received_amount = received_amount, paid_amount return paid_amount, received_amount From a450ce25b9620a9a50468f344fce0bdad93c757a Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 21 Apr 2025 12:26:17 +0530 Subject: [PATCH 38/49] fix: respect field "ignore_user_permissions" property in employee query (cherry picked from commit 91d7bc55be47082aedadf53ce109f088d5945079) --- erpnext/controllers/queries.py | 44 +++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 50d7ad641de..9ff568b92d0 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -8,9 +8,10 @@ from collections import OrderedDict, defaultdict import frappe from frappe import qb, scrub from frappe.desk.reportview import get_filters_cond, get_match_cond +from frappe.permissions import has_permission from frappe.query_builder import Criterion, CustomFunction from frappe.query_builder.functions import Concat, Locate, Sum -from frappe.utils import nowdate, today, unique +from frappe.utils import cint, nowdate, today, unique from pypika import Order import erpnext @@ -20,10 +21,28 @@ from erpnext.stock.get_item_details import _get_item_tax_template # searches for active employees @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def employee_query(doctype, txt, searchfield, start, page_len, filters): +def employee_query( + doctype, + txt, + searchfield, + start, + page_len, + filters, + reference_doctype: str | None = None, + ignore_user_permissions: bool = False, +): doctype = "Employee" conditions = [] fields = get_fields(doctype, ["name", "employee_name"]) + ignore_permissions = False + + if reference_doctype and ignore_user_permissions: + ignore_permissions = has_ignored_field(reference_doctype, doctype) and has_permission( + doctype, + ptype="select" if frappe.only_has_select_perm(doctype) else "read", + ) + + mcond = "" if ignore_permissions else get_match_cond(doctype) return frappe.db.sql( """select {fields} from `tabEmployee` @@ -42,13 +61,32 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): "fields": ", ".join(fields), "key": searchfield, "fcond": get_filters_cond(doctype, filters, conditions), - "mcond": get_match_cond(doctype), + "mcond": mcond, } ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, ) +def has_ignored_field(reference_doctype, doctype): + meta = frappe.get_meta(reference_doctype) + for field in meta.fields: + if not field.ignore_user_permissions: + continue + if field.fieldtype == "Link" and field.options == doctype: + return True + elif field.fieldtype == "Dynamic Link": + options = meta.get_link_doctype(field.fieldname) + if not options: + continue + if isinstance(options, str): + options = options.split("\n") + if doctype in options or "Doctype" in options: + return True + + return False + + # searches for leads which are not converted @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From e752f3f914cb1edbc689497b9c6efb5972272f74 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 22 Apr 2025 13:00:37 +0530 Subject: [PATCH 39/49] chore: added test case for employee query with user permissions (cherry picked from commit 4be975f87c1e651cbec97eab846ef08135941547) # Conflicts: # erpnext/controllers/tests/test_queries.py --- erpnext/controllers/queries.py | 2 +- erpnext/controllers/tests/test_queries.py | 54 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 9ff568b92d0..f3e66cba663 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -81,7 +81,7 @@ def has_ignored_field(reference_doctype, doctype): continue if isinstance(options, str): options = options.split("\n") - if doctype in options or "Doctype" in options: + if doctype in options or "DocType" in options: return True return False diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py index 0ef108e5f7b..ba380a1a3a1 100644 --- a/erpnext/controllers/tests/test_queries.py +++ b/erpnext/controllers/tests/test_queries.py @@ -2,6 +2,9 @@ import unittest from functools import partial import frappe +from frappe.core.doctype.user_permission.test_user_permission import create_user +from frappe.core.doctype.user_permission.user_permission import add_user_permissions +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from erpnext.controllers import queries @@ -81,3 +84,54 @@ class TestQueries(unittest.TestCase): def test_default_uoms(self): self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10) + + def test_employee_query_with_user_permissions(self): + # party field is a dynamic link field in Payment Entry doctype with ignore_user_permissions=0 + ps = make_property_setter( + doctype="Payment Entry", + fieldname="party", + property="ignore_user_permissions", + value=1, + property_type="Check", + ) + ps.save() + + user = create_user("test_employee_query@example.com", ("Accounts User", "HR User")) + add_user_permissions( + { + "user": user.name, + "doctype": "Employee", + "docname": "_T-Employee-00001", + "is_default": 1, + "apply_to_all_doctypes": 1, + "applicable_doctypes": [], + "hide_descendants": 0, + } + ) + + frappe.reload_doc("accounts", "doctype", "payment entry") + + frappe.set_user(user.name) + params = { + "doctype": "Employee", + "txt": "", + "searchfield": "name", + "start": 0, + "page_len": 20, + "filters": None, + "reference_doctype": "Payment Entry", + "ignore_user_permissions": 1, + } + + result = queries.employee_query(**params) + self.assertGreater(len(result), 1) + + ps.delete(ignore_permissions=1, force=1, delete_permanently=1) + frappe.reload_doc("accounts", "doctype", "payment entry") + frappe.clear_cache() + + # only one employee should be returned even though ignore_user_permissions is passed as 1 + result = queries.employee_query(**params) + self.assertEqual(len(result), 1) + + frappe.set_user("Administrator") From f6edd5aa7dd31a3db3ff0775413f3fc522c1f7f2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 21 Apr 2025 19:05:16 +0530 Subject: [PATCH 40/49] fix: backslash in url (cherry picked from commit ecf15130bad830e603c3778408e8770fce651783) # Conflicts: # erpnext/stock/doctype/material_request/material_request.json --- .../doctype/production_plan/production_plan.py | 4 +++- .../doctype/material_request/material_request.json | 10 ++++++++-- .../stock/doctype/material_request/material_request.py | 8 +++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 2e89a191f66..ad5bf5be7d4 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -912,9 +912,11 @@ class ProductionPlan(Document): frappe.flags.mute_messages = False + from urllib.parse import quote_plus + if material_request_list: material_request_list = [ - f"""{m.name}""" + f"""{m.name}""" for m in material_request_list ] msgprint(_("{0} created").format(comma_and(material_request_list))) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 25c765bbced..b8ca285c19f 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -53,13 +53,14 @@ "options": "fa fa-pushpin" }, { + "default": "MAT/MR/.YYYY.-", "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "MAT-MR-.YYYY.-", + "options": "MAT-MR-.YYYY.-\nMAT/MR/.YYYY.-", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -357,7 +358,11 @@ "idx": 70, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-09-15 12:07:24.789471", +======= + "modified": "2025-04-21 18:36:04.827917", +>>>>>>> ecf15130ba (fix: backslash in url) "modified_by": "Administrator", "module": "Stock", "name": "Material Request", @@ -425,10 +430,11 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "search_fields": "status,transaction_date", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", "states": [], "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 54b3e17641e..c206c232431 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -24,14 +24,15 @@ form_grid_templates = {"items": "templates/form_grid/material_request_grid.html" class MaterialRequest(BuyingController): # begin: auto-generated types + # ruff: noqa + # This code is auto-generated. Do not modify anything in this block. from typing import TYPE_CHECKING if TYPE_CHECKING: - from frappe.types import DF - from erpnext.stock.doctype.material_request_item.material_request_item import MaterialRequestItem + from frappe.types import DF amended_from: DF.Link | None company: DF.Link @@ -42,7 +43,7 @@ class MaterialRequest(BuyingController): material_request_type: DF.Literal[ "Purchase", "Material Transfer", "Material Issue", "Manufacture", "Customer Provided" ] - naming_series: DF.Literal["MAT-MR-.YYYY.-"] + naming_series: DF.Literal["MAT-MR-.YYYY.-", "MAT/MR/.YYYY.-"] per_ordered: DF.Percent per_received: DF.Percent scan_barcode: DF.Data | None @@ -70,6 +71,7 @@ class MaterialRequest(BuyingController): transaction_date: DF.Date transfer_status: DF.Literal["", "Not Started", "In Transit", "Completed"] work_order: DF.Link | None + # ruff: noqa # end: auto-generated types def check_if_already_pulled(self): From ad350216663f81dad87198703383972e6841d354 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 22 Apr 2025 15:22:54 +0530 Subject: [PATCH 41/49] fix: use get_url_to_form instead (cherry picked from commit 7a82b37f7605449aadb0c1a53dfde8bdf64beef7) --- .../manufacturing/doctype/production_plan/production_plan.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index ad5bf5be7d4..03845d656f4 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -17,6 +17,7 @@ from frappe.utils import ( comma_and, flt, get_link_to_form, + get_url_to_form, getdate, now_datetime, nowdate, @@ -912,11 +913,9 @@ class ProductionPlan(Document): frappe.flags.mute_messages = False - from urllib.parse import quote_plus - if material_request_list: material_request_list = [ - f"""{m.name}""" + f"""{m.name}""" for m in material_request_list ] msgprint(_("{0} created").format(comma_and(material_request_list))) From a09ab902e5b64b396e455d2a6ed2add7a6892419 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 22 Apr 2025 15:27:09 +0530 Subject: [PATCH 42/49] fix: revert unintended changes (cherry picked from commit eaaf34cda659f61a13d1fe9f15cb71bc91ba10c0) # Conflicts: # erpnext/stock/doctype/material_request/material_request.json --- .../doctype/material_request/material_request.json | 10 ++++++---- .../stock/doctype/material_request/material_request.py | 8 +++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index b8ca285c19f..88344bad8a7 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -53,14 +53,13 @@ "options": "fa fa-pushpin" }, { - "default": "MAT/MR/.YYYY.-", "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "MAT-MR-.YYYY.-\nMAT/MR/.YYYY.-", + "options": "MAT-MR-.YYYY.-", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -358,11 +357,15 @@ "idx": 70, "is_submittable": 1, "links": [], +<<<<<<< HEAD <<<<<<< HEAD "modified": "2023-09-15 12:07:24.789471", ======= "modified": "2025-04-21 18:36:04.827917", >>>>>>> ecf15130ba (fix: backslash in url) +======= + "modified": "2024-12-16 12:46:02.262167", +>>>>>>> eaaf34cda6 (fix: revert unintended changes) "modified_by": "Administrator", "module": "Stock", "name": "Material Request", @@ -430,11 +433,10 @@ } ], "quick_entry": 1, - "row_format": "Dynamic", "search_fields": "status,transaction_date", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", "states": [], "title_field": "title" -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index c206c232431..54b3e17641e 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -24,16 +24,15 @@ form_grid_templates = {"items": "templates/form_grid/material_request_grid.html" class MaterialRequest(BuyingController): # begin: auto-generated types - # ruff: noqa - # This code is auto-generated. Do not modify anything in this block. from typing import TYPE_CHECKING if TYPE_CHECKING: - from erpnext.stock.doctype.material_request_item.material_request_item import MaterialRequestItem from frappe.types import DF + from erpnext.stock.doctype.material_request_item.material_request_item import MaterialRequestItem + amended_from: DF.Link | None company: DF.Link customer: DF.Link | None @@ -43,7 +42,7 @@ class MaterialRequest(BuyingController): material_request_type: DF.Literal[ "Purchase", "Material Transfer", "Material Issue", "Manufacture", "Customer Provided" ] - naming_series: DF.Literal["MAT-MR-.YYYY.-", "MAT/MR/.YYYY.-"] + naming_series: DF.Literal["MAT-MR-.YYYY.-"] per_ordered: DF.Percent per_received: DF.Percent scan_barcode: DF.Data | None @@ -71,7 +70,6 @@ class MaterialRequest(BuyingController): transaction_date: DF.Date transfer_status: DF.Literal["", "Not Started", "In Transit", "Completed"] work_order: DF.Link | None - # ruff: noqa # end: auto-generated types def check_if_already_pulled(self): From 982a68b71a7215168ad8075e900582689229ea2b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 22 Apr 2025 15:50:02 +0530 Subject: [PATCH 43/49] fix: change get_url_to_form to get_link_to_form (cherry picked from commit 5d07beee61102c2b5ab171dabc62dae6400e56c8) --- .../manufacturing/doctype/production_plan/production_plan.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 03845d656f4..ddb3a8860ec 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -915,8 +915,7 @@ class ProductionPlan(Document): if material_request_list: material_request_list = [ - f"""{m.name}""" - for m in material_request_list + get_link_to_form("Material Request", m.name) for m in material_request_list ] msgprint(_("{0} created").format(comma_and(material_request_list))) else: From 4fba4d49d261826ddfbfe992437330c62cd2fe6a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 22 Apr 2025 15:50:51 +0530 Subject: [PATCH 44/49] fix: remove unused import (cherry picked from commit c3d172fac32acb5692f2ca52f98927fb92ad3d1e) --- erpnext/manufacturing/doctype/production_plan/production_plan.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index ddb3a8860ec..681abc8ddde 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -17,7 +17,6 @@ from frappe.utils import ( comma_and, flt, get_link_to_form, - get_url_to_form, getdate, now_datetime, nowdate, From e2f8ca5f875f5f1990147cd837cd9b314ed930d4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Apr 2025 16:46:57 +0530 Subject: [PATCH 45/49] chore: resolve conflict --- .../stock/doctype/material_request/material_request.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 88344bad8a7..8df61fe7fb8 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -357,15 +357,7 @@ "idx": 70, "is_submittable": 1, "links": [], -<<<<<<< HEAD -<<<<<<< HEAD - "modified": "2023-09-15 12:07:24.789471", -======= "modified": "2025-04-21 18:36:04.827917", ->>>>>>> ecf15130ba (fix: backslash in url) -======= - "modified": "2024-12-16 12:46:02.262167", ->>>>>>> eaaf34cda6 (fix: revert unintended changes) "modified_by": "Administrator", "module": "Stock", "name": "Material Request", From 680c221f05791aaed3270be74f1abb667da6b0ef Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Mon, 21 Apr 2025 16:22:14 +0530 Subject: [PATCH 46/49] fix: keep per_billed 100 for billed delivery note after return (cherry picked from commit 8290a83591d7b932c3e313ce3e02d03a80dc1cc3) --- erpnext/controllers/status_updater.py | 2 +- erpnext/controllers/stock_controller.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index e1e4c4ce8f2..cd92a102234 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -84,8 +84,8 @@ status_map = { "Delivery Note": [ ["Draft", None], ["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"], - ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], + ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ], diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index d07723f02df..fbc98a1a2f4 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -988,7 +988,13 @@ class StockController(AccountsController): def update_billing_percentage(self, update_modified=True): target_ref_field = "amount" if self.doctype == "Delivery Note": - target_ref_field = "amount - (returned_qty * rate)" + total_amount = total_returned = 0 + for item in self.items: + total_amount += flt(item.amount) + total_returned += flt(item.returned_qty * item.rate) + + if total_returned < total_amount: + target_ref_field = "(amount - (returned_qty * rate))" self._update_percent_field( { From 2b05ccfa6f0698f5387b8148ccb6c29126a719bf Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Mon, 21 Apr 2025 16:23:32 +0530 Subject: [PATCH 47/49] test: add new unit test to keep per_billed 100 for billed delivery note (cherry picked from commit fe5898a151c5d300dfebe7b89970b91c70f2461d) --- .../delivery_note/test_delivery_note.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 3394e0ce83a..924bc1255d3 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2521,6 +2521,28 @@ class TestDeliveryNote(FrappeTestCase): for d in bundle_data: self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) + def test_delivery_note_per_billed_after_return(self): + from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note + + so = make_sales_order(qty=2) + dn = make_delivery_note(so.name) + dn.submit() + self.assertEqual(dn.per_billed, 0) + + si = make_sales_invoice(dn.name) + si.location = "Test Location" + si.submit() + + dn_return = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, do_not_submit=True) + dn_return.items[0].dn_detail = dn.items[0].name + dn_return.submit() + + returned = frappe.get_doc("Delivery Note", dn_return.name) + returned.update_prevdoc_status() + dn.load_from_db() + self.assertEqual(dn.per_billed, 100) + self.assertEqual(dn.per_returned, 100) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") From 04a1578b53fae5382bb9875b296ad3b1de441efa Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Mon, 21 Apr 2025 16:24:08 +0530 Subject: [PATCH 48/49] refactor: update base_outstanding calculation (cherry picked from commit 02356029a8849448d57b0fe763a6ae7d449583bb) --- erpnext/controllers/accounts_controller.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index eab0064a85e..f576bc91541 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2398,13 +2398,12 @@ class AccountsController(TransactionBase): base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount") ) d.outstanding = d.payment_amount - d.base_outstanding = flt( - d.payment_amount * self.get("conversion_rate"), d.precision("base_outstanding") - ) + d.base_outstanding = d.base_payment_amount elif not d.invoice_portion: d.base_payment_amount = flt( d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount") ) + d.base_outstanding = d.base_payment_amount else: self.fetch_payment_terms_from_order( po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms From 8050e653ab6a5a36d4f5accdee160e7c2045e4b0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:53:10 +0530 Subject: [PATCH 49/49] fix: get total without rounding off tax amounts for distributing discount (backport #47155) Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com> --- .../purchase_invoice/test_purchase_invoice.py | 19 ++--- erpnext/controllers/taxes_and_totals.py | 70 +++++++++------- .../public/js/controllers/taxes_and_totals.js | 81 +++++++++++-------- 3 files changed, 102 insertions(+), 68 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 084a262a890..7c4ba47f9ba 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2688,13 +2688,13 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): To test if after applying discount on grand total, the grand total is calculated correctly without any rounding errors """ - invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True) + invoice = make_purchase_invoice(qty=3, rate=100, do_not_save=True, do_not_submit=True) invoice.append( "items", { "item_code": "_Test Item", - "qty": 1, - "rate": 21.39, + "qty": 3, + "rate": 50.3, }, ) invoice.append( @@ -2703,18 +2703,19 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): "charge_type": "On Net Total", "account_head": "_Test Account VAT - _TC", "description": "VAT", - "rate": 15.5, + "rate": 15, }, ) - # the grand total here will be 255.71 + # the grand total here will be 518.54 invoice.disable_rounded_total = 1 - # apply discount on grand total to adjust the grand total to 255 - invoice.discount_amount = 0.71 + # apply discount on grand total to adjust the grand total to 518 + invoice.discount_amount = 0.54 + invoice.save() - # check if grand total is 496 and not something like 254.99 due to rounding errors - self.assertEqual(invoice.grand_total, 255) + # check if grand total is 518 and not something like 517.99 due to rounding errors + self.assertEqual(invoice.grand_total, 518) def test_apply_discount_on_grand_total_with_previous_row_total_tax(self): """ diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 5e35dd5028e..5543129d323 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -377,20 +377,22 @@ class calculate_taxes_and_totals: self._calculate() def calculate_taxes(self): - self.grand_total_diff = 0 + doc = self.doc + if not doc.get("taxes"): + return # maintain actual tax rate based on idx actual_tax_dict = dict( [ [tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] - for tax in self.doc.get("taxes") + for tax in doc.taxes if tax.charge_type == "Actual" ] ) for n, item in enumerate(self._items): item_tax_map = self._load_item_tax_rate(item.item_tax_rate) - for i, tax in enumerate(self.doc.get("taxes")): + for i, tax in enumerate(doc.taxes): # tax_amount represents the amount of tax for the current step current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map) if frappe.flags.round_row_wise_tax: @@ -425,30 +427,39 @@ class calculate_taxes_and_totals: tax.grand_total_for_current_item = flt(item.net_amount + current_tax_amount) else: tax.grand_total_for_current_item = flt( - self.doc.get("taxes")[i - 1].grand_total_for_current_item + current_tax_amount + doc.taxes[i - 1].grand_total_for_current_item + current_tax_amount ) - # set precision in the last item iteration - if n == len(self._items) - 1: - self.round_off_totals(tax) - self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]) + discount_amount_applied = self.discount_amount_applied + if doc.apply_discount_on == "Grand Total" and ( + discount_amount_applied or doc.discount_amount or doc.additional_discount_percentage + ): + tax_amount_precision = doc.taxes[0].precision("tax_amount") - self.round_off_base_values(tax) - self.set_cumulative_total(i, tax) + for i, tax in enumerate(doc.taxes): + if discount_amount_applied: + tax.tax_amount_after_discount_amount = flt( + tax.tax_amount_after_discount_amount, tax_amount_precision + ) - self._set_in_company_currency(tax, ["total"]) + self.set_cumulative_total(i, tax) - # adjust Discount Amount loss in last tax iteration - if ( - i == (len(self.doc.get("taxes")) - 1) - and self.discount_amount_applied - and self.doc.discount_amount - and self.doc.apply_discount_on == "Grand Total" - ): - self.grand_total_diff = flt( - self.doc.grand_total - flt(self.doc.discount_amount) - tax.total, - self.doc.precision("rounding_adjustment"), - ) + if not discount_amount_applied: + self.grand_total_for_distributing_discount = doc.taxes[-1].total + else: + self.grand_total_diff = flt( + self.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[-1].total, + doc.precision("grand_total"), + ) + + for i, tax in enumerate(doc.taxes): + self.round_off_totals(tax) + self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]) + + self.round_off_base_values(tax) + self.set_cumulative_total(i, tax) + + self._set_in_company_currency(tax, ["total"]) def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax): # if just for valuation, do not add the tax amount in total @@ -571,16 +582,20 @@ class calculate_taxes_and_totals: if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")): self.grand_total_diff = diff + else: + self.grand_total_diff = 0 def calculate_totals(self): + grand_total_diff = getattr(self, "grand_total_diff", 0) + if self.doc.get("taxes"): - self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff + self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff else: self.doc.grand_total = flt(self.doc.net_total) if self.doc.get("taxes"): self.doc.total_taxes_and_charges = flt( - self.doc.grand_total - self.doc.net_total - self.grand_total_diff, + self.doc.grand_total - self.doc.net_total - grand_total_diff, self.doc.precision("total_taxes_and_charges"), ) else: @@ -725,7 +740,8 @@ class calculate_taxes_and_totals: self.doc.base_discount_amount = 0 def get_total_for_discount_amount(self): - if self.doc.apply_discount_on == "Net Total": + doc = self.doc + if doc.apply_discount_on == "Net Total" or not doc.get("taxes"): return self.doc.net_total total_actual_tax = 0 @@ -745,7 +761,7 @@ class calculate_taxes_and_totals: "cumulative_tax_amount": total_actual_tax, } - for tax in self.doc.get("taxes"): + for tax in doc.taxes: if tax.charge_type in ["Actual", "On Item Quantity"]: update_actual_tax_dict(tax, tax.tax_amount) continue @@ -764,7 +780,7 @@ class calculate_taxes_and_totals: ) update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100) - return self.doc.grand_total - total_actual_tax + return getattr(self, "grand_total_for_distributing_discount", doc.grand_total) - total_actual_tax def calculate_total_advance(self): if not self.doc.docstatus.is_cancelled(): diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 4bc787f85ff..66b348e9eeb 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -342,12 +342,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } calculate_taxes() { + const doc = this.frm.doc; + if (!doc.taxes?.length) return; + var me = this; - this.grand_total_diff = 0; var actual_tax_dict = {}; // maintain actual tax rate based on idx - $.each(this.frm.doc["taxes"] || [], function(i, tax) { + $.each(doc.taxes, function(i, tax) { if (tax.charge_type == "Actual") { actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax)); } @@ -355,7 +357,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { $.each(this.frm._items || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); - $.each(me.frm.doc["taxes"] || [], function(i, tax) { + $.each(doc.taxes, function(i, tax) { // tax_amount represents the amount of tax for the current step var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map); if (frappe.flags.round_row_wise_tax) { @@ -400,29 +402,40 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { tax.grand_total_for_current_item = flt(me.frm.doc["taxes"][i-1].grand_total_for_current_item + current_tax_amount); } - - // set precision in the last item iteration - if (n == me.frm._items.length - 1) { - me.round_off_totals(tax); - me.set_in_company_currency(tax, - ["tax_amount", "tax_amount_after_discount_amount"]); - - me.round_off_base_values(tax); - - // in tax.total, accumulate grand total for each item - me.set_cumulative_total(i, tax); - - me.set_in_company_currency(tax, ["total"]); - - // adjust Discount Amount loss in last tax iteration - if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied - && me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) { - me.grand_total_diff = flt(me.frm.doc.grand_total - - flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment")); - } - } }); }); + + const discount_amount_applied = this.discount_amount_applied; + if (doc.apply_discount_on === "Grand Total" && (discount_amount_applied || doc.discount_amount || doc.additional_discount_percentage)) { + const tax_amount_precision = precision("tax_amount", doc.taxes[0]); + + for (const [i, tax] of doc.taxes.entries()) { + if (discount_amount_applied) + tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, tax_amount_precision); + + this.set_cumulative_total(i, tax); + } + + if (!this.discount_amount_applied) { + this.grand_total_for_distributing_discount = doc.taxes[doc.taxes.length - 1].total; + } else { + this.grand_total_diff = flt( + this.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[doc.taxes.length - 1].total, precision("grand_total")); + } + } + + for (const [i, tax] of doc.taxes.entries()) { + me.round_off_totals(tax); + me.set_in_company_currency(tax, + ["tax_amount", "tax_amount_after_discount_amount"]); + + me.round_off_base_values(tax); + + // in tax.total, accumulate grand total for each tax + me.set_cumulative_total(i, tax); + + me.set_in_company_currency(tax, ["total"]); + } } set_cumulative_total(row_idx, tax) { @@ -571,10 +584,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { calculate_totals() { // Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency - var me = this; - var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0; + const me = this; + const tax_count = this.frm.doc.taxes?.length; + const grand_total_diff = this.grand_total_diff || 0; + this.frm.doc.grand_total = flt(tax_count - ? this.frm.doc["taxes"][tax_count - 1].total + this.grand_total_diff + ? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff : this.frm.doc.net_total); if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) { @@ -606,7 +621,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total - - this.grand_total_diff, precision("total_taxes_and_charges")); + - grand_total_diff, precision("total_taxes_and_charges")); this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]); @@ -729,8 +744,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } get_total_for_discount_amount() { - if(this.frm.doc.apply_discount_on == "Net Total") - return this.frm.doc.net_total; + const doc = this.frm.doc; + + if (doc.apply_discount_on == "Net Total" || !doc.taxes?.length) + return doc.net_total; let total_actual_tax = 0.0; let actual_taxes_dict = {}; @@ -745,7 +762,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { }; } - $.each(this.frm.doc["taxes"] || [], function(i, tax) { + doc.taxes.forEach(tax => { if (["Actual", "On Item Quantity"].includes(tax.charge_type)) { update_actual_taxes_dict(tax, tax.tax_amount); return; @@ -760,7 +777,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100); }); - return this.frm.doc.grand_total - total_actual_tax; + return (this.grand_total_for_distributing_discount || doc.grand_total) - total_actual_tax; } calculate_total_advance(update_paid_amount) {