From 695e2bcfbcb27d0502bfd0620d5a4d3268175e14 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Sat, 14 May 2022 11:06:24 -0400 Subject: [PATCH 01/77] fix: don't reserve qty on sales return. --- erpnext/stock/stock_balance.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index e05d1c3a29..cf5acbdd55 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -97,7 +97,7 @@ def get_reserved_qty(item_code, warehouse): reserved_qty = frappe.db.sql( """ select - sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty) / so_item_qty)) + sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty - so_item_returned_qty) / so_item_qty)) from ( (select @@ -112,6 +112,11 @@ def get_reserved_qty(item_code, warehouse): where name = dnpi.parent_detail_docname and delivered_by_supplier = 0 ) as so_item_delivered_qty, + ( + select returned_qty from `tabSales Order Item` + where name = dnpi.parent_detail_docname + and delivered_by_supplier = 0 + ) as so_item_returned_qty, parent, name from ( @@ -125,7 +130,8 @@ def get_reserved_qty(item_code, warehouse): ) dnpi) union (select stock_qty as dnpi_qty, qty as so_item_qty, - delivered_qty as so_item_delivered_qty, parent, name + delivered_qty as so_item_delivered_qty, + returned_qty as so_item_returned_qty, parent, name from `tabSales Order Item` so_item where item_code = %s and warehouse = %s and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0) From 47f867b77a5a3f74fd0fe482de815f870aed6e7d Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Mon, 16 May 2022 14:27:45 -0400 Subject: [PATCH 02/77] test: don't reserve qty on sales return. --- .../delivery_note/test_delivery_note.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index fffcdca380..ba47fa3a46 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1064,6 +1064,32 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.items[0].rate, rate) + def test_reserved_qty(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note + from erpnext.stock.stock_balance import get_reserved_qty + + item = make_item() + + so = make_sales_order(items=[{"item_code": item.name}]) + + soi = so.items[0] + + # Test that item qty has been reserved on submit of sales order. + self.assertEqual(get_reserved_qty(soi.item_code, soi.warehouse), soi.qty) + + dn = make_delivery_note(so.name) + dn.submit() + + # Test that item qty is no longer reserved since qty has been delivered. + self.assertEqual(get_reserved_qty(soi.item_code, soi.warehouse), 0) + + sr = make_return_doc("Delivery Note", dn.name) + sr.submit() + + # Test that item qty is not reserved on sales return. + self.assertEqual(get_reserved_qty(soi.item_code, soi.warehouse), 0) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") From 4d0fa85a755eb86d5011b0f31dcb6d01c0a74f4d Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Mon, 16 May 2022 19:45:59 -0400 Subject: [PATCH 03/77] fix(test): update_prevdoc_status --- erpnext/stock/doctype/delivery_note/test_delivery_note.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index ba47fa3a46..e55bdef7c9 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1087,6 +1087,9 @@ class TestDeliveryNote(FrappeTestCase): sr = make_return_doc("Delivery Note", dn.name) sr.submit() + returned = frappe.get_doc("Delivery Note", sr.name) + returned.update_prevdoc_status() + # Test that item qty is not reserved on sales return. self.assertEqual(get_reserved_qty(soi.item_code, soi.warehouse), 0) From 5a402a570915d5c6e6100e1b6444b9536db2bf56 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Tue, 17 May 2022 13:32:26 -0400 Subject: [PATCH 04/77] fix(test): use unique item in sales order. --- erpnext/stock/doctype/delivery_note/test_delivery_note.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index e55bdef7c9..c74b570fdc 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1071,10 +1071,13 @@ class TestDeliveryNote(FrappeTestCase): item = make_item() - so = make_sales_order(items=[{"item_code": item.name}]) + so = make_sales_order(item_code=item.name) soi = so.items[0] + # Make qty avl for test. + make_stock_entry(item_code=item.name, target=soi.warehouse, qty=10, basic_rate=100) + # Test that item qty has been reserved on submit of sales order. self.assertEqual(get_reserved_qty(soi.item_code, soi.warehouse), soi.qty) From d259c2e36ab52d4674187002f5ccd7828f774d9b Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Fri, 17 Jun 2022 11:50:54 -0400 Subject: [PATCH 05/77] fix: merge conflict --- .../delivery_note/test_delivery_note.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index fffcdca380..ce90eec016 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1064,6 +1064,38 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.items[0].rate, rate) + def test_reserved_qty(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note + from erpnext.stock.stock_balance import get_reserved_qty + + item = make_item().name + warehouse = "_Test Warehouse - _TC" + qty_to_reserve = 5 + + so = make_sales_order(item_code=item, qty=qty_to_reserve) + + # Make qty avl for test. + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, basic_rate=100) + + # Test that item qty has been reserved on submit of sales order. + self.assertEqual(get_reserved_qty(item, warehouse), qty_to_reserve) + + dn = make_delivery_note(so.name) + dn.save().submit() + + # Test that item qty is no longer reserved since qty has been delivered. + self.assertEqual(get_reserved_qty(item, warehouse), 0) + + dn_return = make_return_doc("Delivery Note", dn.name) + dn_return.save().submit() + + returned = frappe.get_doc("Delivery Note", dn_return.name) + returned.update_prevdoc_status() + + # Test that item qty is not reserved on sales return. + self.assertEqual(get_reserved_qty(item, warehouse), 0) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") From f3f26d2c786670a9127010ba07da1665caf0737c Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Fri, 17 Jun 2022 11:51:21 -0400 Subject: [PATCH 06/77] refactor: get_reserved_qty query builder --- erpnext/stock/stock_balance.py | 127 ++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 48 deletions(-) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index cf5acbdd55..2f5bac91d3 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -3,7 +3,11 @@ import frappe +from frappe.query_builder import DocType +from frappe.query_builder.functions import Sum +from frappe.query_builder.utils import Table from frappe.utils import cstr, flt, now, nowdate, nowtime +from pypika.queries import QueryBuilder from erpnext.controllers.stock_controller import create_repost_item_valuation_entry @@ -94,57 +98,84 @@ def get_balance_qty_from_sle(item_code, warehouse): def get_reserved_qty(item_code, warehouse): - reserved_qty = frappe.db.sql( - """ - select - sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty - so_item_returned_qty) / so_item_qty)) - from - ( - (select - qty as dnpi_qty, + SalesOrder = DocType("Sales Order") + SalesOrderItem = DocType("Sales Order Item") + PackedItem = DocType("Packed Item") + + def append_open_so_query(q: QueryBuilder, child_table: Table) -> QueryBuilder: + return ( + q.inner_join(SalesOrder) + .on(SalesOrder.name == child_table.parent) + .where(SalesOrder.docstatus == 1) + .where(SalesOrder.status != "Closed") + ) + + tab = ( + frappe.qb.from_(SalesOrderItem) + .select( + SalesOrderItem.stock_qty.as_("dnpi_qty"), + SalesOrderItem.qty.as_("so_item_qty"), + SalesOrderItem.delivered_qty.as_("so_item_delivered_qty"), + SalesOrderItem.returned_qty.as_("so_item_returned_qty"), + SalesOrderItem.parent, + SalesOrderItem.name, + ) + .where(SalesOrderItem.item_code == item_code) + .where(SalesOrderItem.warehouse == warehouse) + ) + tab = append_open_so_query(tab, SalesOrderItem) + + dnpi = ( + frappe.qb.from_(PackedItem) + .select(PackedItem.qty, PackedItem.parent_detail_docname, PackedItem.parent, PackedItem.name) + .where(PackedItem.item_code == item_code) + .where(PackedItem.warehouse == warehouse) + ) + dnpi = append_open_so_query(dnpi, PackedItem) + + qty_queries = {} + for key, so_item_field in [ + ("so_item_qty", "qty"), + ("so_item_delivered_qty", "delivered_qty"), + ("so_item_returned_qty", "returned_qty"), + ]: + qty_queries.update( + { + key: ( + frappe.qb.from_(SalesOrderItem) + .select(SalesOrderItem[so_item_field]) + .where(SalesOrderItem.name == dnpi.parent_detail_docname) + .where(SalesOrderItem.delivered_by_supplier == 0) + ) + } + ) + + dnpi_parent = frappe.qb.from_(dnpi).select(dnpi.qty.as_("dnpi_qty")) + for key, query in qty_queries.items(): + dnpi_parent = dnpi_parent.select(query.as_(key)) + dnpi_parent = dnpi_parent.select(dnpi.parent, dnpi.name) + + dnpi_parent = dnpi_parent + tab + + q = ( + frappe.qb.from_(dnpi_parent) + .select( + Sum( + dnpi_parent.dnpi_qty + * ( ( - select qty from `tabSales Order Item` - where name = dnpi.parent_detail_docname - and (delivered_by_supplier is null or delivered_by_supplier = 0) - ) as so_item_qty, - ( - select delivered_qty from `tabSales Order Item` - where name = dnpi.parent_detail_docname - and delivered_by_supplier = 0 - ) as so_item_delivered_qty, - ( - select returned_qty from `tabSales Order Item` - where name = dnpi.parent_detail_docname - and delivered_by_supplier = 0 - ) as so_item_returned_qty, - parent, name - from - ( - select qty, parent_detail_docname, parent, name - from `tabPacked Item` dnpi_in - where item_code = %s and warehouse = %s - and parenttype="Sales Order" - and item_code != parent_item - and exists (select * from `tabSales Order` so - where name = dnpi_in.parent and docstatus = 1 and status != 'Closed') - ) dnpi) - union - (select stock_qty as dnpi_qty, qty as so_item_qty, - delivered_qty as so_item_delivered_qty, - returned_qty as so_item_returned_qty, parent, name - from `tabSales Order Item` so_item - where item_code = %s and warehouse = %s - and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0) - and exists(select * from `tabSales Order` so - where so.name = so_item.parent and so.docstatus = 1 - and so.status != 'Closed')) - ) tab - where - so_item_qty >= so_item_delivered_qty - """, - (item_code, warehouse, item_code, warehouse), + dnpi_parent.so_item_qty + - dnpi_parent.so_item_delivered_qty + - dnpi_parent.so_item_returned_qty + ) + / dnpi_parent.so_item_qty + ) + ) + ) + .where(dnpi_parent.so_item_qty >= dnpi_parent.so_item_delivered_qty) ) + reserved_qty = q.run() return flt(reserved_qty[0][0]) if reserved_qty else 0 From 494bbf01245c72750421813834430b16a3b82739 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Fri, 17 Jun 2022 13:51:33 -0400 Subject: [PATCH 07/77] feat: add checkbox to reserve qty on sales return --- .../selling_settings/selling_settings.json | 9 ++- .../delivery_note/test_delivery_note.py | 18 +++++- erpnext/stock/stock_balance.py | 59 ++++++++++--------- 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 2abb169b8a..7e2d1c7436 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -20,6 +20,7 @@ "editable_price_list_rate", "validate_selling_price", "editable_bundle_item_rates", + "dont_reserve_sales_order_qty_on_sales_return", "sales_transactions_settings_section", "so_required", "dn_required", @@ -172,6 +173,12 @@ "fieldname": "enable_discount_accounting", "fieldtype": "Check", "label": "Enable Discount Accounting for Selling" + }, + { + "default": "0", + "fieldname": "dont_reserve_sales_order_qty_on_sales_return", + "fieldtype": "Check", + "label": "Don't Reserve Sales Order Qty on Sales Return" } ], "icon": "fa fa-cog", @@ -179,7 +186,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-05-31 19:39:48.398738", + "modified": "2022-06-17 12:30:57.221570", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index ce90eec016..8dd2d11757 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1064,11 +1064,23 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.items[0].rate, rate) - def test_reserved_qty(self): + def test_reserve_qty_on_sales_return(self): + frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) + self.reserved_qty_check() + + def test_dont_reserve_qty_on_sales_return(self): + frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 1) + self.reserved_qty_check() + + def reserved_qty_check(self): from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.stock.stock_balance import get_reserved_qty + dont_reserve_qty = frappe.db.get_single_value( + "Selling Settings", "dont_reserve_sales_order_qty_on_sales_return" + ) + item = make_item().name warehouse = "_Test Warehouse - _TC" qty_to_reserve = 5 @@ -1093,8 +1105,8 @@ class TestDeliveryNote(FrappeTestCase): returned = frappe.get_doc("Delivery Note", dn_return.name) returned.update_prevdoc_status() - # Test that item qty is not reserved on sales return. - self.assertEqual(get_reserved_qty(item, warehouse), 0) + # Test that item qty is not reserved on sales return, if selling setting don't reserve qty is checked. + self.assertEqual(get_reserved_qty(item, warehouse), 0 if dont_reserve_qty else qty_to_reserve) def create_delivery_note(**args): diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 2f5bac91d3..fbb5bf8e16 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -98,10 +98,6 @@ def get_balance_qty_from_sle(item_code, warehouse): def get_reserved_qty(item_code, warehouse): - SalesOrder = DocType("Sales Order") - SalesOrderItem = DocType("Sales Order Item") - PackedItem = DocType("Packed Item") - def append_open_so_query(q: QueryBuilder, child_table: Table) -> QueryBuilder: return ( q.inner_join(SalesOrder) @@ -110,19 +106,29 @@ def get_reserved_qty(item_code, warehouse): .where(SalesOrder.status != "Closed") ) + SalesOrder = DocType("Sales Order") + SalesOrderItem = DocType("Sales Order Item") + PackedItem = DocType("Packed Item") + + dont_reserve_qty_on_sales_return = frappe.db.get_single_value( + "Selling Settings", "dont_reserve_sales_order_qty_on_sales_return" + ) + tab = ( frappe.qb.from_(SalesOrderItem) - .select( - SalesOrderItem.stock_qty.as_("dnpi_qty"), - SalesOrderItem.qty.as_("so_item_qty"), - SalesOrderItem.delivered_qty.as_("so_item_delivered_qty"), - SalesOrderItem.returned_qty.as_("so_item_returned_qty"), - SalesOrderItem.parent, - SalesOrderItem.name, - ) .where(SalesOrderItem.item_code == item_code) .where(SalesOrderItem.warehouse == warehouse) ) + for field, cond in [ + (SalesOrderItem.stock_qty.as_("dnpi_qty"), 1), + (SalesOrderItem.qty.as_("so_item_qty"), 1), + (SalesOrderItem.delivered_qty.as_("so_item_delivered_qty"), 1), + (SalesOrderItem.returned_qty.as_("so_item_returned_qty"), dont_reserve_qty_on_sales_return), + (SalesOrderItem.parent, 1), + (SalesOrderItem.name, 1), + ]: + if cond: + tab = tab.select(field) tab = append_open_so_query(tab, SalesOrderItem) dnpi = ( @@ -131,28 +137,23 @@ def get_reserved_qty(item_code, warehouse): .where(PackedItem.item_code == item_code) .where(PackedItem.warehouse == warehouse) ) - dnpi = append_open_so_query(dnpi, PackedItem) + append_open_so_query(dnpi, PackedItem) - qty_queries = {} - for key, so_item_field in [ - ("so_item_qty", "qty"), - ("so_item_delivered_qty", "delivered_qty"), - ("so_item_returned_qty", "returned_qty"), + dnpi_parent = frappe.qb.from_(dnpi).select(dnpi.qty.as_("dnpi_qty")) + for key, so_item_field, cond in [ + ("so_item_qty", "qty", 1), + ("so_item_delivered_qty", "delivered_qty", 1), + ("so_item_returned_qty", "returned_qty", dont_reserve_qty_on_sales_return), ]: - qty_queries.update( - { - key: ( + if cond: + dnpi_parent = dnpi_parent.select( + ( frappe.qb.from_(SalesOrderItem) .select(SalesOrderItem[so_item_field]) .where(SalesOrderItem.name == dnpi.parent_detail_docname) .where(SalesOrderItem.delivered_by_supplier == 0) - ) - } - ) - - dnpi_parent = frappe.qb.from_(dnpi).select(dnpi.qty.as_("dnpi_qty")) - for key, query in qty_queries.items(): - dnpi_parent = dnpi_parent.select(query.as_(key)) + ).as_(key) + ) dnpi_parent = dnpi_parent.select(dnpi.parent, dnpi.name) dnpi_parent = dnpi_parent + tab @@ -166,7 +167,7 @@ def get_reserved_qty(item_code, warehouse): ( dnpi_parent.so_item_qty - dnpi_parent.so_item_delivered_qty - - dnpi_parent.so_item_returned_qty + - (dnpi_parent.so_item_returned_qty if dont_reserve_qty_on_sales_return else 0) ) / dnpi_parent.so_item_qty ) From 0328874018003cbd49e5653be0d97240bd464499 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Fri, 17 Jun 2022 14:45:33 -0400 Subject: [PATCH 08/77] fix: syntax missing ) --- erpnext/stock/stock_balance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 682ac5d507..0ab530a2cb 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -174,6 +174,7 @@ def get_reserved_qty(item_code, warehouse): ) ) .where(dnpi_parent.so_item_qty >= dnpi_parent.so_item_delivered_qty) + ) reserved_qty = q.run() return flt(reserved_qty[0][0]) if reserved_qty else 0 From 591b5917a937d68ff90f35a7e562095f549b5960 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Fri, 17 Jun 2022 15:27:17 -0400 Subject: [PATCH 09/77] fix: re-assign after append query --- erpnext/stock/stock_balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 0ab530a2cb..a8e056c992 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -137,7 +137,7 @@ def get_reserved_qty(item_code, warehouse): .where(PackedItem.item_code == item_code) .where(PackedItem.warehouse == warehouse) ) - append_open_so_query(dnpi, PackedItem) + dnpi = append_open_so_query(dnpi, PackedItem) dnpi_parent = frappe.qb.from_(dnpi).select(dnpi.qty.as_("dnpi_qty")) for key, so_item_field, cond in [ From 179e2d2c74e6888396dec202c0246f43046874d6 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Fri, 17 Jun 2022 15:29:32 -0400 Subject: [PATCH 10/77] fix: reset selling setting --- erpnext/stock/doctype/delivery_note/test_delivery_note.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 8dd2d11757..442ebab726 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1108,6 +1108,9 @@ class TestDeliveryNote(FrappeTestCase): # Test that item qty is not reserved on sales return, if selling setting don't reserve qty is checked. self.assertEqual(get_reserved_qty(item, warehouse), 0 if dont_reserve_qty else qty_to_reserve) + def tearDown(self): + frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") From 6a6c5603755a97b54af5d50b18e447dcb8d8ac7e Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Fri, 17 Jun 2022 15:51:11 -0400 Subject: [PATCH 11/77] fix: move to transcation settings --- .../selling/doctype/selling_settings/selling_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 7e2d1c7436..73bfbd32cc 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -20,7 +20,6 @@ "editable_price_list_rate", "validate_selling_price", "editable_bundle_item_rates", - "dont_reserve_sales_order_qty_on_sales_return", "sales_transactions_settings_section", "so_required", "dn_required", @@ -28,6 +27,7 @@ "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", + "dont_reserve_sales_order_qty_on_sales_return", "hide_tax_id", "enable_discount_accounting" ], @@ -186,7 +186,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-06-17 12:30:57.221570", + "modified": "2022-06-17 15:50:43.968334", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From ef6480803bb4437c4b0aa508d86af2e3f2142449 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 22 Jun 2022 14:10:40 -0400 Subject: [PATCH 12/77] chore: linter --- .../delivery_note/test_delivery_note.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 3611d0b415..f2bcdb3948 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1064,32 +1064,32 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.items[0].rate, rate) - def test_internal_transfer_precision_gle(self): - from erpnext.selling.doctype.customer.test_customer import create_internal_customer - - item = make_item(properties={"valuation_method": "Moving Average"}).name - company = "_Test Company with perpetual inventory" - warehouse = "Stores - TCP1" - target = "Finished Goods - TCP1" - customer = create_internal_customer(represents_company=company) - - # average rate = 128.015 - rates = [101.45, 150.46, 138.25, 121.9] - - for rate in rates: - make_stock_entry(item_code=item, target=warehouse, qty=1, rate=rate) - - dn = create_delivery_note( - item_code=item, - company=company, - customer=customer, - qty=4, - warehouse=warehouse, - target_warehouse=target, - ) - self.assertFalse( - frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype}) - ) + def test_internal_transfer_precision_gle(self): + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + + item = make_item(properties={"valuation_method": "Moving Average"}).name + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + target = "Finished Goods - TCP1" + customer = create_internal_customer(represents_company=company) + + # average rate = 128.015 + rates = [101.45, 150.46, 138.25, 121.9] + + for rate in rates: + make_stock_entry(item_code=item, target=warehouse, qty=1, rate=rate) + + dn = create_delivery_note( + item_code=item, + company=company, + customer=customer, + qty=4, + warehouse=warehouse, + target_warehouse=target, + ) + self.assertFalse( + frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype}) + ) def test_reserve_qty_on_sales_return(self): frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) From 768c3a49278e35abc31a04a0b87d2dcd2e8794d8 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 3 Mar 2023 13:21:38 +0530 Subject: [PATCH 13/77] fix: Taxes aren't discounted on early payment discount - Deductions in payment entry must be split into income loss and tax loss - Compute total discount in percentage, makes discounting different amounts proportionately easier --- .../doctype/payment_entry/payment_entry.py | 92 ++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index cd5b6d5ce2..91d31ab0ec 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1669,7 +1669,7 @@ def get_payment_entry( dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc ) - paid_amount, received_amount, discount_amount = apply_early_payment_discount( + paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount( paid_amount, received_amount, doc ) @@ -1769,7 +1769,9 @@ def get_payment_entry( if party_account and bank: pe.set_exchange_rate(ref_doc=reference_doc) pe.set_amounts() - if discount_amount: + + discount_amount = set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount) + if discount_amount > 0: pe.set_gain_or_loss( account_details={ "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), @@ -1891,6 +1893,7 @@ def set_paid_amount_and_received_amount( def apply_early_payment_discount(paid_amount, received_amount, doc): total_discount = 0 + valid_discounts = [] eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule @@ -1911,13 +1914,96 @@ def apply_early_payment_discount(paid_amount, received_amount, doc): received_amount -= discount_amount paid_amount -= discount_amount_in_foreign_currency + valid_discounts.append({"type": term.discount_type, "discount": term.discount}) total_discount += discount_amount if total_discount: money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency")) frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1) - return paid_amount, received_amount, total_discount + return paid_amount, received_amount, total_discount, valid_discounts + + +def set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount): + """Split early bird discount deductions into Income Loss & Tax Loss.""" + if not (discount_amount and valid_discounts): + return discount_amount + + total_discount_percent = get_total_discount_percent(doc, valid_discounts) + + if not total_discount_percent: + return discount_amount + + loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent) + loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent) + + return flt(discount_amount - (loss_on_income + loss_on_taxes)) + + +def get_total_discount_percent(doc, valid_discounts) -> float: + """Get total percentage and amount discount applied as a percentage.""" + total_discount_percent = ( + sum( + discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage" + ) + or 0.0 + ) + + # Operate in percentages only as it makes the income & tax split easier + total_discount_amount = ( + sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount") + or 0.0 + ) + + if total_discount_amount: + discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100 + total_discount_percent += discount_percentage + return total_discount_percent + + return total_discount_percent + + +def add_income_discount_loss(pe, doc, total_discount_percent) -> float: + loss_on_income = flt(doc.get("total") * (total_discount_percent / 100), doc.precision("total")) + pe.append( + "deductions", + { + "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), + "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), + "amount": loss_on_income, + }, + ) + return loss_on_income + + +def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float: + tax_discount_loss = {} + total_tax_loss = 0 + precision = doc.precision("tax_amount_after_discount_amount", "taxes") + + # The same account head could be used more than once + for tax in doc.get("taxes", []): + tax_loss = flt( + tax.get("tax_amount_after_discount_amount") * (total_discount_percenatage / 100), precision + ) + account = tax.get("account_head") + if not tax_discount_loss.get(account): + tax_discount_loss[account] = tax_loss + else: + tax_discount_loss[account] += tax_loss + + for account, loss in tax_discount_loss.items(): + total_tax_loss += loss + pe.append( + "deductions", + { + "account": account, + "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), + "amount": loss, + }, + ) + + return total_tax_loss def get_reference_as_per_payment_terms( From 75ec0a0a85a010415765518f5a9e36bb13d08b22 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 3 Mar 2023 14:13:27 +0530 Subject: [PATCH 14/77] fix: Recalculate difference amount after setting deductions --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 91d31ab0ec..cf1cc0a839 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1772,6 +1772,7 @@ def get_payment_entry( discount_amount = set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount) if discount_amount > 0: + # Set pending discount amount in deductions pe.set_gain_or_loss( account_details={ "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), @@ -1780,7 +1781,8 @@ def get_payment_entry( "amount": discount_amount * (-1 if payment_type == "Pay" else 1), } ) - pe.set_difference_amount() + + pe.set_difference_amount() return pe From dc2998f5442613e3c3624493896686fc75f3c388 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 3 Mar 2023 17:24:43 +0530 Subject: [PATCH 15/77] fix: Set deductions in base currency - Use field precision to get more accurate values --- .../doctype/payment_entry/payment_entry.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index cf1cc0a839..05399d0cec 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1772,13 +1772,14 @@ def get_payment_entry( discount_amount = set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount) if discount_amount > 0: - # Set pending discount amount in deductions + # Set pending base discount amount in deductions + positive_negative = -1 if payment_type == "Pay" else 1 pe.set_gain_or_loss( account_details={ "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": discount_amount * (-1 if payment_type == "Pay" else 1), + "amount": discount_amount * positive_negative * doc.get("conversion_rate", 1), } ) @@ -1966,19 +1967,22 @@ def get_total_discount_percent(doc, valid_discounts) -> float: def add_income_discount_loss(pe, doc, total_discount_percent) -> float: - loss_on_income = flt(doc.get("total") * (total_discount_percent / 100), doc.precision("total")) + """Add loss on income discount in base currency.""" + precision = doc.precision("total") + loss_on_income = flt(doc.get("total") * (total_discount_percent / 100), precision) pe.append( "deductions", { "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": loss_on_income, + "amount": flt(loss_on_income * doc.get("conversion_rate", 1), precision), }, ) return loss_on_income def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float: + """Add loss on tax discount in base currency.""" tax_discount_loss = {} total_tax_loss = 0 precision = doc.precision("tax_amount_after_discount_amount", "taxes") @@ -2001,7 +2005,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float: { "account": account, "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": loss, + "amount": flt(loss * doc.get("conversion_rate", 1), precision), }, ) From 6190b4cf63415222562c99156c4a3d5278844a75 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Fri, 3 Mar 2023 09:42:29 -0500 Subject: [PATCH 16/77] chore: revert unrelated changes. --- .../selling/doctype/sales_order/sales_order.json | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 9559f134c1..ccea8407ab 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -17,8 +17,6 @@ "customer_name", "tax_id", "order_type", - "col_breaktest123", - "test_my_field", "column_break_7", "transaction_date", "delivery_date", @@ -250,15 +248,6 @@ "print_hide": 1, "reqd": 1 }, - { - "fieldname": "col_breaktest123", - "fieldtype": "Column Break" - }, - { - "fieldname": "test_my_field", - "fieldtype": "Data", - "label": "Test My Field" - }, { "fieldname": "column_break1", "fieldtype": "Column Break", @@ -1654,7 +1643,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-02-13 11:59:00.681780", + "modified": "2022-12-12 18:34:00.681780", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1733,4 +1722,4 @@ "title_field": "customer_name", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file From 2ae58342907c0cfb9ae7658176e1549fb51d1cb3 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 6 Mar 2023 14:54:46 +0530 Subject: [PATCH 17/77] fix: Back update discounted amount in Invoice based on discount type - Discount value was always trated as a percentage on back updation --- .../doctype/payment_entry/payment_entry.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 05399d0cec..6e612eeee7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -424,15 +424,28 @@ class PaymentEntry(AccountsController): payment_schedule = frappe.get_all( "Payment Schedule", filters={"parent": ref.reference_name}, - fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"], + fields=[ + "paid_amount", + "payment_amount", + "payment_term", + "discount", + "outstanding", + "discount_type", + ], ) for term in payment_schedule: invoice_key = (term.payment_term, ref.reference_name) invoice_paid_amount_map.setdefault(invoice_key, {}) invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding - invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * ( - term.discount / 100 - ) + if not (term.discount_type and term.discount): + continue + + if term.discount_type == "Percentage": + invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * ( + term.discount / 100 + ) + else: + invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1): if not invoice_paid_amount_map.get(key): From c217bb201878327fb6dfa341fbf65c19761916a5 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 6 Mar 2023 15:02:32 +0530 Subject: [PATCH 18/77] test: PE from SI with early payment discount amount & PE assertions in discount % test --- .../payment_entry/test_payment_entry.py | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 123b5dfd51..fe80bad3d1 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -260,6 +260,14 @@ class TestPaymentEntry(FrappeTestCase): si.submit() pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + + self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount") + self.assertEqual(pe.references[0].allocated_amount, 236.0) + self.assertEqual(pe.paid_amount, 212.4) + self.assertEqual(pe.deductions[0].amount, 20.0) # Loss on Income + self.assertEqual(pe.deductions[1].amount, 3.6) # Loss on Tax + self.assertEqual(pe.deductions[1].account, "_Test Account Service Tax - _TC") + pe.submit() si.load_from_db() @@ -269,6 +277,46 @@ class TestPaymentEntry(FrappeTestCase): self.assertEqual(si.payment_schedule[0].outstanding, 0) self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6) + def test_payment_entry_against_payment_terms_with_discount_amount(self): + si = create_sales_invoice(do_not_save=1, qty=1, rate=200) + + si.payment_terms_template = "Test Discount Amount Template" + create_payment_terms_template_with_discount( + name="30 Credit Days with Rs.50 Discount", + discount_type="Amount", + discount=50, + template_name="Test Discount Amount Template", + ) + frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC") + + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 18, + }, + ) + si.save() + si.submit() + + pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + self.assertEqual(pe.references[0].allocated_amount, 236.0) + self.assertEqual(pe.paid_amount, 186) + self.assertEqual(pe.deductions[0].amount, 42.37) # Loss on Income + self.assertEqual(pe.deductions[1].amount, 7.63) # Loss on Tax + self.assertEqual(pe.deductions[1].account, "_Test Account Service Tax - _TC") + + pe.submit() + si.load_from_db() + + self.assertEqual(si.payment_schedule[0].payment_amount, 236.0) + self.assertEqual(si.payment_schedule[0].paid_amount, 186) + self.assertEqual(si.payment_schedule[0].outstanding, 0) + self.assertEqual(si.payment_schedule[0].discounted_amount, 50) + def test_payment_against_purchase_invoice_to_check_status(self): pi = make_purchase_invoice( supplier="_Test Supplier USD", @@ -839,24 +887,27 @@ def create_payment_terms_template(): ).insert() -def create_payment_terms_template_with_discount(): +def create_payment_terms_template_with_discount( + name=None, discount_type=None, discount=None, template_name=None +): + create_payment_term(name or "30 Credit Days with 10% Discount") + template_name = template_name or "Test Discount Template" - create_payment_term("30 Credit Days with 10% Discount") - - if not frappe.db.exists("Payment Terms Template", "Test Discount Template"): - payment_term_template = frappe.get_doc( + if not frappe.db.exists("Payment Terms Template", template_name): + frappe.get_doc( { "doctype": "Payment Terms Template", - "template_name": "Test Discount Template", + "template_name": template_name, "allocate_payment_based_on_payment_terms": 1, "terms": [ { "doctype": "Payment Terms Template Detail", - "payment_term": "30 Credit Days with 10% Discount", + "payment_term": name or "30 Credit Days with 10% Discount", "invoice_portion": 100, "credit_days_based_on": "Day(s) after invoice date", "credit_days": 2, - "discount": 10, + "discount_type": discount_type or "Percentage", + "discount": discount or 10, "discount_validity_based_on": "Day(s) after invoice date", "discount_validity": 1, } From 7f2e7badffab44355a4525369eb1a044b2f9e5c1 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 6 Mar 2023 17:43:40 +0530 Subject: [PATCH 19/77] fix: Set deduction amount in company currency on Doctype - Even via JS, deductions amount is always in company currency - Since there is nothing dynamic about this field, set it in the doctype spec itself - fixed: Inconsistency between label currency and field currency formatted value --- .../doctype/payment_entry/payment_entry.js | 2 -- .../payment_entry_deduction.json | 29 +++++++------------ 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 91374ae217..5a56a6b004 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -245,8 +245,6 @@ frappe.ui.form.on('Payment Entry', { frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"], party_account_currency, "references"); - frm.set_currency_labels(["amount"], company_currency, "deductions"); - cur_frm.set_df_property("source_exchange_rate", "description", ("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency)); diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json index 61a1462dd7..1c31829f0e 100644 --- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json +++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json @@ -3,6 +3,7 @@ "creation": "2016-06-15 15:56:30.815503", "doctype": "DocType", "editable_grid": 1, + "engine": "InnoDB", "field_order": [ "account", "cost_center", @@ -17,9 +18,7 @@ "in_list_view": 1, "label": "Account", "options": "Account", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "cost_center", @@ -28,37 +27,30 @@ "label": "Cost Center", "options": "Cost Center", "print_hide": 1, - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "Amount", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "label": "Amount (Company Currency)", + "options": "Company:company:default_currency", + "reqd": 1 }, { "fieldname": "column_break_2", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "description", "fieldtype": "Small Text", - "label": "Description", - "show_days": 1, - "show_seconds": 1 + "label": "Description" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-12 20:38:08.110674", + "modified": "2023-03-06 07:11:57.739619", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Deduction", @@ -66,5 +58,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file From f02fc8acf0d50fcc178b713a1385595a40cb19f0 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 7 Mar 2023 12:12:45 +0530 Subject: [PATCH 20/77] fix: Don't add to deductions if amount is 0 - misc: better docstring --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 6e612eeee7..f927b1b887 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1941,7 +1941,7 @@ def apply_early_payment_discount(paid_amount, received_amount, doc): def set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount): - """Split early bird discount deductions into Income Loss & Tax Loss.""" + """Split early payment discount into Income Loss & Tax Loss.""" if not (discount_amount and valid_discounts): return discount_amount @@ -2013,12 +2013,16 @@ def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float: for account, loss in tax_discount_loss.items(): total_tax_loss += loss + amount = flt(loss * doc.get("conversion_rate", 1), precision) + if amount == 0.0: + continue + pe.append( "deductions", { "account": account, "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": flt(loss * doc.get("conversion_rate", 1), precision), + "amount": amount, }, ) From 761f68d7bf0b8539f26a79993245c8ffcbcde5f1 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 8 Mar 2023 17:20:48 +0530 Subject: [PATCH 21/77] fix: Paid amount must be discounted considering accounting currency - Accounting is in the same currency if party currency and company currency is the same - If accounting is in the same currency, paid and recvd amount is in the base currency - Then, discount amount must also be in the base currency as it is deducted from paid amount - Received amount must be in base currency if not multi currency - cleanup: Deductions setting broken into smaller functions --- .../doctype/payment_entry/payment_entry.py | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index f927b1b887..152ec4df79 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1683,7 +1683,7 @@ def get_payment_entry( ) paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount( - paid_amount, received_amount, doc + paid_amount, received_amount, doc, party_account_currency ) pe = frappe.new_doc("Payment Entry") @@ -1783,17 +1783,10 @@ def get_payment_entry( pe.set_exchange_rate(ref_doc=reference_doc) pe.set_amounts() - discount_amount = set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount) - if discount_amount > 0: - # Set pending base discount amount in deductions - positive_negative = -1 if payment_type == "Pay" else 1 - pe.set_gain_or_loss( - account_details={ - "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), - "cost_center": pe.cost_center - or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": discount_amount * positive_negative * doc.get("conversion_rate", 1), - } + if discount_amount: + base_total_discount_loss = set_early_payment_discount_loss(pe, doc, valid_discounts) + set_pending_discount_loss( + pe, doc, discount_amount, base_total_discount_loss, party_account_currency ) pe.set_difference_amount() @@ -1907,7 +1900,7 @@ def set_paid_amount_and_received_amount( return paid_amount, received_amount -def apply_early_payment_discount(paid_amount, received_amount, doc): +def apply_early_payment_discount(paid_amount, received_amount, doc, party_account_currency): total_discount = 0 valid_discounts = [] eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] @@ -1916,12 +1909,17 @@ def apply_early_payment_discount(paid_amount, received_amount, doc): if doc.doctype in eligible_for_payments and has_payment_schedule: for term in doc.payment_schedule: if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: + is_multi_currency = party_account_currency != doc.company_currency + if term.discount_type == "Percentage": - discount_amount = flt(doc.get("grand_total")) * (term.discount / 100) + grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total") + discount_amount = flt(grand_total) * (term.discount / 100) else: discount_amount = term.discount - discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1) + # if accounting is done in the same currency, paid_amount = received_amount + conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1 + discount_amount_in_foreign_currency = discount_amount * conversion_rate if doc.doctype == "Sales Invoice": paid_amount -= discount_amount @@ -1940,20 +1938,38 @@ def apply_early_payment_discount(paid_amount, received_amount, doc): return paid_amount, received_amount, total_discount, valid_discounts -def set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount): - """Split early payment discount into Income Loss & Tax Loss.""" - if not (discount_amount and valid_discounts): - return discount_amount +def set_pending_discount_loss( + pe, doc, discount_amount, base_total_discount_loss, party_account_currency +): + # If multi-currency, get base discount amount to adjust with base currency deductions/losses + if party_account_currency != doc.company_currency: + discount_amount = discount_amount * doc.get("conversion_rate", 1) + discount_amount -= base_total_discount_loss + + # If pending base discount amount, set it in deductions + if discount_amount > 0.0: + positive_negative = -1 if pe.payment_type == "Pay" else 1 + pe.set_gain_or_loss( + account_details={ + "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), + "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), + "amount": discount_amount * positive_negative, + } + ) + + +def set_early_payment_discount_loss(pe, doc, valid_discounts) -> float: + """Split early payment discount into Income Loss & Tax Loss.""" total_discount_percent = get_total_discount_percent(doc, valid_discounts) if not total_discount_percent: - return discount_amount + return 0.0 loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent) loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent) - return flt(discount_amount - (loss_on_income + loss_on_taxes)) + return flt(loss_on_income + loss_on_taxes) def get_total_discount_percent(doc, valid_discounts) -> float: From b09c2381ca144c63098d0fedf79c92fa5f7b929a Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 9 Mar 2023 16:25:45 +0530 Subject: [PATCH 22/77] fix: Multi-currency SI with base currency PE - Return total discount loss in base currency - Allocate payment based on terms: Set allocated amount in references table in base currency if accounting is in that currency - Allocate payment based on terms: While back updating set paid amount (payment schedule) in transaction currency always - minor: discount msgprint in correct currency --- .../doctype/payment_entry/payment_entry.py | 87 ++++++++++++++----- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 152ec4df79..9dfc674ed7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -416,7 +416,7 @@ class PaymentEntry(AccountsController): for ref in self.get("references"): if ref.payment_term and ref.reference_name: - key = (ref.payment_term, ref.reference_name) + key = (ref.payment_term, ref.reference_name, ref.reference_doctype) invoice_payment_amount_map.setdefault(key, 0.0) invoice_payment_amount_map[key] += ref.allocated_amount @@ -434,7 +434,7 @@ class PaymentEntry(AccountsController): ], ) for term in payment_schedule: - invoice_key = (term.payment_term, ref.reference_name) + invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype) invoice_paid_amount_map.setdefault(invoice_key, {}) invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding if not (term.discount_type and term.discount): @@ -451,6 +451,10 @@ class PaymentEntry(AccountsController): if not invoice_paid_amount_map.get(key): frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1])) + allocated_amount = self.get_allocated_amount_in_transaction_currency( + allocated_amount, key[2], key[1] + ) + outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding")) discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt")) @@ -485,6 +489,33 @@ class PaymentEntry(AccountsController): (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]), ) + def get_allocated_amount_in_transaction_currency( + self, allocated_amount, reference_doctype, reference_docname + ): + """ + Payment Entry could be in base currency while reference's payment schedule + is always in transaction currency. + E.g. + * SI with base=INR and currency=USD + * SI with payment schedule in USD + * PE in INR (accounting done in base currency) + """ + ref_currency, ref_exchange_rate = frappe.db.get_value( + reference_doctype, reference_docname, ["currency", "conversion_rate"] + ) + is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency + # PE in different currency + reference_is_multi_currency = self.paid_from_account_currency != ref_currency + + if not (is_single_currency and reference_is_multi_currency): + return allocated_amount + + allocated_amount = flt( + allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount") + ) + + return allocated_amount + def set_status(self): if self.docstatus == 2: self.status = "Cancelled" @@ -1731,7 +1762,7 @@ def get_payment_entry( ): for reference in get_reference_as_per_payment_terms( - doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount + doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency ): pe.append("references", reference) else: @@ -1905,11 +1936,11 @@ def apply_early_payment_discount(paid_amount, received_amount, doc, party_accoun valid_discounts = [] eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule + is_multi_currency = party_account_currency != doc.company_currency if doc.doctype in eligible_for_payments and has_payment_schedule: for term in doc.payment_schedule: if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: - is_multi_currency = party_account_currency != doc.company_currency if term.discount_type == "Percentage": grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total") @@ -1932,7 +1963,8 @@ def apply_early_payment_discount(paid_amount, received_amount, doc, party_accoun total_discount += discount_amount if total_discount: - money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency")) + currency = doc.get("currency") if is_multi_currency else doc.company_currency + money = frappe.utils.fmt_money(total_discount, currency=currency) frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1) return paid_amount, received_amount, total_discount, valid_discounts @@ -1952,7 +1984,6 @@ def set_pending_discount_loss( positive_negative = -1 if pe.payment_type == "Pay" else 1 pe.set_gain_or_loss( account_details={ - "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), "amount": discount_amount * positive_negative, } @@ -1966,10 +1997,10 @@ def set_early_payment_discount_loss(pe, doc, valid_discounts) -> float: if not total_discount_percent: return 0.0 - loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent) - loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent) + base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent) + base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent) - return flt(loss_on_income + loss_on_taxes) + return flt(base_loss_on_income + base_loss_on_taxes) def get_total_discount_percent(doc, valid_discounts) -> float: @@ -1999,38 +2030,41 @@ def add_income_discount_loss(pe, doc, total_discount_percent) -> float: """Add loss on income discount in base currency.""" precision = doc.precision("total") loss_on_income = flt(doc.get("total") * (total_discount_percent / 100), precision) + base_loss_on_income = flt(loss_on_income * doc.get("conversion_rate", 1), precision) + pe.append( "deductions", { "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": flt(loss_on_income * doc.get("conversion_rate", 1), precision), + "amount": base_loss_on_income, }, ) - return loss_on_income + return base_loss_on_income -def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float: +def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float: """Add loss on tax discount in base currency.""" tax_discount_loss = {} - total_tax_loss = 0 + base_total_tax_loss = 0 precision = doc.precision("tax_amount_after_discount_amount", "taxes") # The same account head could be used more than once for tax in doc.get("taxes", []): tax_loss = flt( - tax.get("tax_amount_after_discount_amount") * (total_discount_percenatage / 100), precision + tax.get("tax_amount_after_discount_amount") * (total_discount_percentage / 100), precision ) + base_tax_loss = flt(tax_loss * doc.get("conversion_rate", 1), precision) + account = tax.get("account_head") if not tax_discount_loss.get(account): - tax_discount_loss[account] = tax_loss + tax_discount_loss[account] = base_tax_loss else: - tax_discount_loss[account] += tax_loss + tax_discount_loss[account] += base_tax_loss for account, loss in tax_discount_loss.items(): - total_tax_loss += loss - amount = flt(loss * doc.get("conversion_rate", 1), precision) - if amount == 0.0: + base_total_tax_loss += loss + if loss == 0.0: continue pe.append( @@ -2038,21 +2072,30 @@ def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float: { "account": account, "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": amount, + "amount": loss, }, ) - return total_tax_loss + return base_total_tax_loss def get_reference_as_per_payment_terms( - payment_schedule, dt, dn, doc, grand_total, outstanding_amount + payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency ): references = [] + is_multi_currency_acc = (doc.currency != doc.company_currency) and ( + party_account_currency != doc.company_currency + ) + for payment_term in payment_schedule: payment_term_outstanding = flt( payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount") ) + if not is_multi_currency_acc: + # If accounting is done in company currency for multi-currency transaction + payment_term_outstanding = flt( + payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount") + ) if payment_term_outstanding: references.append( From 9abf0ef615d38d806e27b0c2fcce48125fd75fa1 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 9 Mar 2023 16:32:36 +0530 Subject: [PATCH 23/77] test: Multi currency SI with multi-currency accounting and single currency accounting + Early payment discount --- .../payment_entry/test_payment_entry.py | 128 +++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index fe80bad3d1..6e5c25ee99 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -5,7 +5,7 @@ import unittest import frappe from frappe import qb -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import flt, nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import ( @@ -317,6 +317,132 @@ class TestPaymentEntry(FrappeTestCase): self.assertEqual(si.payment_schedule[0].outstanding, 0) self.assertEqual(si.payment_schedule[0].discounted_amount, 50) + @change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1}, + ) + def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount( + self, + ): + """ + 1. Multi-currency SI with single currency accounting (company currency) + 2. PE with early payment discount + 3. Test if Paid Amount is calculated in company currency + 4. Test if deductions are calculated in company currency + + SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency. + """ + si = create_sales_invoice( + customer="_Test Customer", + currency="USD", + conversion_rate=50, + do_not_save=1, + ) + create_payment_terms_template_with_discount() + si.payment_terms_template = "Test Discount Template" + + frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC") + si.save() + si.submit() + + pe = get_payment_entry( + "Sales Invoice", + si.name, + bank_account="_Test Bank - _TC", + ) + pe.reference_no = si.name + pe.reference_date = nowdate() + + # Early payment discount loss on income + self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency + self.assertEqual(pe.received_amount, 4500.0) + self.assertEqual(pe.deductions[0].amount, 500.0) + self.assertEqual(pe.deductions[0].account, "Write Off - _TC") + self.assertEqual(pe.difference_amount, 0.0) + + pe.insert() + pe.submit() + + expected_gle = dict( + (d[0], d) + for d in [ + ["Debtors - _TC", 0, 5000, si.name], + ["_Test Bank - _TC", 4500, 0, None], + ["Write Off - _TC", 500.0, 0, None], + ] + ) + + self.validate_gl_entries(pe.name, expected_gle) + + outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) + self.assertEqual(outstanding_amount, 0) + + def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self): + """ + 1. Multi-currency SI with multi-currency accounting + 2. PE with early payment discount and also exchange loss + 3. Test if Paid Amount is calculated in transaction currency + 4. Test if deductions are calculated in base/company currency + 5. Test if exchange loss is reflected in difference + """ + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + do_not_save=1, + ) + create_payment_terms_template_with_discount() + si.payment_terms_template = "Test Discount Template" + + frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC") + si.save() + si.submit() + + pe = get_payment_entry( + "Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700 + ) + pe.reference_no = si.name + pe.reference_date = nowdate() + + # Early payment discount loss on income + self.assertEqual(pe.paid_amount, 90.0) + self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss) + self.assertEqual(pe.deductions[0].amount, 500.0) + self.assertEqual(pe.deductions[0].account, "Write Off - _TC") + + # Exchange loss + self.assertEqual(pe.difference_amount, 300.0) + + pe.append( + "deductions", + { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 300.0, + }, + ) + + pe.insert() + pe.submit() + + self.assertEqual(pe.difference_amount, 0.0) + + expected_gle = dict( + (d[0], d) + for d in [ + ["_Test Receivable USD - _TC", 0, 5000, si.name], + ["_Test Bank - _TC", 4200, 0, None], + ["Write Off - _TC", 500.0, 0, None], + ["_Test Exchange Gain/Loss - _TC", 300.0, 0, None], + ] + ) + + self.validate_gl_entries(pe.name, expected_gle) + + outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) + self.assertEqual(outstanding_amount, 0) + def test_payment_against_purchase_invoice_to_check_status(self): pi = make_purchase_invoice( supplier="_Test Supplier USD", From caa1a3dccf66b8ff379a4482841e8309f4a7fa6d Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 13 Mar 2023 13:55:55 +0530 Subject: [PATCH 24/77] fix: Handle rounding more gracefully - Round off pending discount loss to avoid miniscule losses rounded to 0.0 that are added in deductions - Use base amounts to calculate base losses instead of using conversion factor which increases rounding error - Round of total base loss instead of individual income and tax losses to reduce rounding error - Use default round off account for pending rounding loss in deductions --- .../doctype/payment_entry/payment_entry.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 9dfc674ed7..8e47063002 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1977,13 +1977,15 @@ def set_pending_discount_loss( if party_account_currency != doc.company_currency: discount_amount = discount_amount * doc.get("conversion_rate", 1) - discount_amount -= base_total_discount_loss + # Avoid considering miniscule losses + discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total")) - # If pending base discount amount, set it in deductions + # If pending base discount amount (mostly rounding loss), set it in deductions if discount_amount > 0.0: positive_negative = -1 if pe.payment_type == "Pay" else 1 pe.set_gain_or_loss( account_details={ + "account": frappe.get_cached_value("Company", pe.company, "round_off_account"), "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), "amount": discount_amount * positive_negative, } @@ -2000,7 +2002,8 @@ def set_early_payment_discount_loss(pe, doc, valid_discounts) -> float: base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent) base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent) - return flt(base_loss_on_income + base_loss_on_taxes) + # Round off total loss rather than individual losses to reduce rounding error + return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total")) def get_total_discount_percent(doc, valid_discounts) -> float: @@ -2029,18 +2032,18 @@ def get_total_discount_percent(doc, valid_discounts) -> float: def add_income_discount_loss(pe, doc, total_discount_percent) -> float: """Add loss on income discount in base currency.""" precision = doc.precision("total") - loss_on_income = flt(doc.get("total") * (total_discount_percent / 100), precision) - base_loss_on_income = flt(loss_on_income * doc.get("conversion_rate", 1), precision) + base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100) pe.append( "deductions", { "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": base_loss_on_income, + "amount": flt(base_loss_on_income, precision), }, ) - return base_loss_on_income + + return base_loss_on_income # Return loss without rounding def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float: @@ -2051,10 +2054,9 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float: # The same account head could be used more than once for tax in doc.get("taxes", []): - tax_loss = flt( - tax.get("tax_amount_after_discount_amount") * (total_discount_percentage / 100), precision + base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * ( + total_discount_percentage / 100 ) - base_tax_loss = flt(tax_loss * doc.get("conversion_rate", 1), precision) account = tax.get("account_head") if not tax_discount_loss.get(account): @@ -2072,11 +2074,11 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float: { "account": account, "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": loss, + "amount": flt(loss, precision), }, ) - return base_total_tax_loss + return base_total_tax_loss # Return loss without rounding def get_reference_as_per_payment_terms( From d6d0163514882a9d7ae16a61be54b7776c001e94 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 14 Mar 2023 13:40:55 +0530 Subject: [PATCH 25/77] fix: Provision to apply early payment discount if payment is recorded late - Party could have paid on time but payment is recorded late - Prompt for reference date so that discount is applied while mapping - Prompt only if discount in payment schedule of valid doctypes - test: Reference date and impact on PE - `make_payment_entry` (JS) must be able to access `this` --- .../doctype/payment_entry/payment_entry.py | 19 +++++-- .../payment_entry/test_payment_entry.py | 9 ++++ .../purchase_invoice/purchase_invoice.js | 6 ++- .../doctype/sales_invoice/sales_invoice.js | 9 ++-- .../doctype/purchase_order/purchase_order.js | 6 ++- erpnext/public/js/controllers/transaction.js | 52 ++++++++++++++++--- 6 files changed, 86 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 8e47063002..15fd0c61eb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1686,7 +1686,14 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre @frappe.whitelist() def get_payment_entry( - dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None + dt, + dn, + party_amount=None, + bank_account=None, + bank_amount=None, + party_type=None, + payment_type=None, + reference_date=None, ): reference_doc = None doc = frappe.get_doc(dt, dn) @@ -1713,8 +1720,9 @@ def get_payment_entry( dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc ) + reference_date = getdate(reference_date) paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount( - paid_amount, received_amount, doc, party_account_currency + paid_amount, received_amount, doc, party_account_currency, reference_date ) pe = frappe.new_doc("Payment Entry") @@ -1722,6 +1730,7 @@ def get_payment_entry( pe.company = doc.company pe.cost_center = doc.get("cost_center") pe.posting_date = nowdate() + pe.reference_date = reference_date pe.mode_of_payment = doc.get("mode_of_payment") pe.party_type = party_type pe.party = doc.get(scrub(party_type)) @@ -1931,7 +1940,9 @@ def set_paid_amount_and_received_amount( return paid_amount, received_amount -def apply_early_payment_discount(paid_amount, received_amount, doc, party_account_currency): +def apply_early_payment_discount( + paid_amount, received_amount, doc, party_account_currency, reference_date +): total_discount = 0 valid_discounts = [] eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] @@ -1940,7 +1951,7 @@ def apply_early_payment_discount(paid_amount, received_amount, doc, party_accoun if doc.doctype in eligible_for_payments and has_payment_schedule: for term in doc.payment_schedule: - if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: + if not term.discounted_amount and term.discount and reference_date <= term.discount_date: if term.discount_type == "Percentage": grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total") diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 6e5c25ee99..ef57c99bda 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -302,6 +302,15 @@ class TestPaymentEntry(FrappeTestCase): si.save() si.submit() + # Set reference date past discount cut off date + pe_1 = get_payment_entry( + "Sales Invoice", + si.name, + bank_account="_Test Cash - _TC", + reference_date=frappe.utils.add_days(si.posting_date, 2), + ) + self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied + pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") self.assertEqual(pe.references[0].allocated_amount, 236.0) self.assertEqual(pe.paid_amount, 186) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index e2b4a1ad5b..5c9168bf9c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -82,7 +82,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. if(doc.docstatus == 1 && doc.outstanding_amount != 0 && !(doc.is_return && doc.return_against) && !doc.on_hold) { - this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create')); + this.frm.add_custom_button( + __('Payment'), + () => this.make_payment_entry(), + __('Create') + ); cur_frm.page.set_inner_btn_group_as_primary(__('Create')); } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 47e3f9b935..56e412b297 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -93,9 +93,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e if (doc.docstatus == 1 && doc.outstanding_amount!=0 && !(cint(doc.is_return) && doc.return_against)) { - cur_frm.add_custom_button(__('Payment'), - this.make_payment_entry, __('Create')); - cur_frm.page.set_inner_btn_group_as_primary(__('Create')); + this.frm.add_custom_button( + __('Payment'), + () => this.make_payment_entry(), + __('Create') + ); + this.frm.page.set_inner_btn_group_as_primary(__('Create')); } if(doc.docstatus==1 && !doc.is_return) { diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 47089f7d85..c6c9f1f98a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -236,7 +236,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e this.make_purchase_invoice, __('Create')); if(flt(doc.per_billed) < 100 && doc.status != "Delivered") { - cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); + this.frm.add_custom_button( + __('Payment'), + () => this.make_payment_entry(), + __('Create') + ); } if(flt(doc.per_billed) < 100) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 8d69ea0c99..0bd4d91c6f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1897,20 +1897,60 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } make_payment_entry() { + let via_journal_entry = this.frm.doc.__onload && this.frm.doc.__onload.make_payment_via_journal_entry; + if(this.has_discount_in_schedule() && !via_journal_entry) { + // If early payment discount is applied, ask user for reference date + this.prompt_user_for_reference_date(); + } else { + this.make_mapped_payment_entry(); + } + } + + make_mapped_payment_entry(args) { + var me = this; + args = args || { "dt": this.frm.doc.doctype, "dn": this.frm.doc.name }; return frappe.call({ - method: cur_frm.cscript.get_method_for_payment(), - args: { - "dt": cur_frm.doc.doctype, - "dn": cur_frm.doc.name - }, + method: me.get_method_for_payment(), + args: args, callback: function(r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - // cur_frm.refresh_fields() } }); } + prompt_user_for_reference_date(){ + var me = this; + frappe.prompt({ + label: __("Cheque/Reference Date"), + fieldname: "reference_date", + fieldtype: "Date", + reqd: 1, + }, (values) => { + let args = { + "dt": me.frm.doc.doctype, + "dn": me.frm.doc.name, + "reference_date": values.reference_date + } + me.make_mapped_payment_entry(args); + }, + __("Reference Date for Early Payment Discount"), + __("Continue") + ); + } + + has_discount_in_schedule() { + let is_eligible = in_list( + ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"], + this.frm.doctype + ); + let has_payment_schedule = this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length; + if(!is_eligible || !has_payment_schedule) return false; + + let has_discount = this.frm.doc.payment_schedule.some(row => row.discount_date); + return has_discount; + } + make_quality_inspection() { let data = []; const fields = [ From b04a101c11d8c3757303c21dd1496256ca01a240 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 17 Mar 2023 13:53:49 +0530 Subject: [PATCH 26/77] fix: incorrect `Opening Value` in `Stock Balance` report --- .../report/stock_balance/stock_balance.py | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 0fc642ef20..66991a907f 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, TypedDict import frappe from frappe import _ -from frappe.query_builder.functions import CombineDatetime +from frappe.query_builder.functions import Coalesce, CombineDatetime from frappe.utils import cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of @@ -322,6 +322,34 @@ def get_stock_ledger_entries(filters: StockBalanceFilter, items: List[str]) -> L return query.run(as_dict=True) +def get_opening_vouchers(to_date): + opening_vouchers = {"Stock Entry": [], "Stock Reconciliation": []} + + se = frappe.qb.DocType("Stock Entry") + sr = frappe.qb.DocType("Stock Reconciliation") + + vouchers_data = ( + frappe.qb.from_( + ( + frappe.qb.from_(se) + .select(se.name, Coalesce("Stock Entry").as_("voucher_type")) + .where((se.docstatus == 1) & (se.posting_date <= to_date) & (se.is_opening == "Yes")) + ) + + ( + frappe.qb.from_(sr) + .select(sr.name, Coalesce("Stock Reconciliation").as_("voucher_type")) + .where((sr.docstatus == 1) & (sr.posting_date <= to_date) & (sr.purpose == "Opening Stock")) + ) + ).select("voucher_type", "name") + ).run(as_dict=True) + + if vouchers_data: + for d in vouchers_data: + opening_vouchers[d.voucher_type].append(d.name) + + return opening_vouchers + + def get_inventory_dimension_fields(): return [dimension.fieldname for dimension in get_inventory_dimensions()] @@ -330,9 +358,8 @@ def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]): iwb_map = {} from_date = getdate(filters.get("from_date")) to_date = getdate(filters.get("to_date")) - + opening_vouchers = get_opening_vouchers(to_date) float_precision = cint(frappe.db.get_default("float_precision")) or 3 - inventory_dimensions = get_inventory_dimension_fields() for d in sle: @@ -363,11 +390,7 @@ def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]): value_diff = flt(d.stock_value_difference) - if d.posting_date < from_date or ( - d.posting_date == from_date - and d.voucher_type == "Stock Reconciliation" - and frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock" - ): + if d.posting_date < from_date or d.voucher_no in opening_vouchers.get(d.voucher_type, []): qty_dict.opening_qty += qty_diff qty_dict.opening_val += value_diff From 216a46bd6615aab47a30ff79ddf78503080121c1 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 27 Mar 2023 16:11:00 +0530 Subject: [PATCH 27/77] feat: Make Tax loss booking optional - Checkbox in Accounts Settings - Apply checkbox in PE deductions setting logic - Adjust tests --- .../accounts_settings/accounts_settings.json | 10 +++++- .../doctype/payment_entry/payment_entry.py | 17 +++++++--- .../payment_entry/test_payment_entry.py | 34 ++++++++++++++----- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 3f985b640b..c0eed18ad1 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -31,6 +31,7 @@ "determine_address_tax_category_from", "column_break_19", "add_taxes_from_item_tax_template", + "book_tax_discount_loss", "print_settings", "show_inclusive_tax_in_print", "column_break_12", @@ -360,6 +361,13 @@ "fieldname": "show_balance_in_coa", "fieldtype": "Check", "label": "Show Balances in Chart Of Accounts" + }, + { + "default": "0", + "description": "Split Early Payment Discount Loss into Income and Tax Loss", + "fieldname": "book_tax_discount_loss", + "fieldtype": "Check", + "label": "Book Tax Loss on Early Payment Discount" } ], "icon": "icon-cog", @@ -367,7 +375,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-01-02 12:07:42.434214", + "modified": "2023-03-28 09:50:20.375233", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 15fd0c61eb..c34bddd77e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1824,7 +1824,10 @@ def get_payment_entry( pe.set_amounts() if discount_amount: - base_total_discount_loss = set_early_payment_discount_loss(pe, doc, valid_discounts) + base_total_discount_loss = 0 + if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"): + base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts) + set_pending_discount_loss( pe, doc, discount_amount, base_total_discount_loss, party_account_currency ) @@ -1991,19 +1994,25 @@ def set_pending_discount_loss( # Avoid considering miniscule losses discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total")) - # If pending base discount amount (mostly rounding loss), set it in deductions + # Set base discount amount (discount loss/pending rounding loss) in deductions if discount_amount > 0.0: positive_negative = -1 if pe.payment_type == "Pay" else 1 + + # If tax loss booking is enabled, pending loss will be rounding loss. + # Otherwise it will be the total discount loss. + book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss") + account_type = "round_off_account" if book_tax_loss else "default_discount_account" + pe.set_gain_or_loss( account_details={ - "account": frappe.get_cached_value("Company", pe.company, "round_off_account"), + "account": frappe.get_cached_value("Company", pe.company, account_type), "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), "amount": discount_amount * positive_negative, } ) -def set_early_payment_discount_loss(pe, doc, valid_discounts) -> float: +def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float: """Split early payment discount into Income Loss & Tax Loss.""" total_discount_percent = get_total_discount_percent(doc, valid_discounts) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index ef57c99bda..67049c47ad 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -256,17 +256,24 @@ class TestPaymentEntry(FrappeTestCase): }, ) si.save() - si.submit() + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1) + pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + + self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount") + self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0) + self.assertEqual(pe_with_tax_loss.paid_amount, 212.4) + self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income + self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax + self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC") + + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0) pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") - self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount") self.assertEqual(pe.references[0].allocated_amount, 236.0) self.assertEqual(pe.paid_amount, 212.4) - self.assertEqual(pe.deductions[0].amount, 20.0) # Loss on Income - self.assertEqual(pe.deductions[1].amount, 3.6) # Loss on Tax - self.assertEqual(pe.deductions[1].account, "_Test Account Service Tax - _TC") + self.assertEqual(pe.deductions[0].amount, 23.6) pe.submit() si.load_from_db() @@ -311,12 +318,18 @@ class TestPaymentEntry(FrappeTestCase): ) self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied + # Test if tax loss is booked on enabling configuration + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1) + pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income + self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax + self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC") + + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0) pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") self.assertEqual(pe.references[0].allocated_amount, 236.0) self.assertEqual(pe.paid_amount, 186) - self.assertEqual(pe.deductions[0].amount, 42.37) # Loss on Income - self.assertEqual(pe.deductions[1].amount, 7.63) # Loss on Tax - self.assertEqual(pe.deductions[1].account, "_Test Account Service Tax - _TC") + self.assertEqual(pe.deductions[0].amount, 50.0) pe.submit() si.load_from_db() @@ -328,7 +341,10 @@ class TestPaymentEntry(FrappeTestCase): @change_settings( "Accounts Settings", - {"allow_multi_currency_invoices_against_single_party_account": 1}, + { + "allow_multi_currency_invoices_against_single_party_account": 1, + "book_tax_discount_loss": 1, + }, ) def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount( self, From 17f80801681373e7be3b502e833f75632e0f276b Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Sat, 25 Mar 2023 00:24:53 -0400 Subject: [PATCH 28/77] chore: revert get_reserve_qty method --- erpnext/stock/stock_balance.py | 122 ++++++++++++--------------------- 1 file changed, 42 insertions(+), 80 deletions(-) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index ab6e18e967..439ed7a8e0 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -3,11 +3,7 @@ import frappe -from frappe.query_builder import DocType -from frappe.query_builder.functions import Sum -from frappe.query_builder.utils import Table from frappe.utils import cstr, flt, now, nowdate, nowtime -from pypika.queries import QueryBuilder from erpnext.controllers.stock_controller import create_repost_item_valuation_entry @@ -98,85 +94,51 @@ def get_balance_qty_from_sle(item_code, warehouse): def get_reserved_qty(item_code, warehouse): - def append_open_so_query(q: QueryBuilder, child_table: Table) -> QueryBuilder: - return ( - q.inner_join(SalesOrder) - .on(SalesOrder.name == child_table.parent) - .where(SalesOrder.docstatus == 1) - .where(SalesOrder.status.notin(["On Hold", "Closed"])) - ) - - SalesOrder = DocType("Sales Order") - SalesOrderItem = DocType("Sales Order Item") - PackedItem = DocType("Packed Item") - - dont_reserve_qty_on_sales_return = frappe.db.get_single_value( - "Selling Settings", "dont_reserve_sales_order_qty_on_sales_return" - ) - - tab = ( - frappe.qb.from_(SalesOrderItem) - .where(SalesOrderItem.item_code == item_code) - .where(SalesOrderItem.warehouse == warehouse) - ) - for field, cond in [ - (SalesOrderItem.stock_qty.as_("dnpi_qty"), 1), - (SalesOrderItem.qty.as_("so_item_qty"), 1), - (SalesOrderItem.delivered_qty.as_("so_item_delivered_qty"), 1), - (SalesOrderItem.returned_qty.as_("so_item_returned_qty"), dont_reserve_qty_on_sales_return), - (SalesOrderItem.parent, 1), - (SalesOrderItem.name, 1), - ]: - if cond: - tab = tab.select(field) - tab = append_open_so_query(tab, SalesOrderItem) - - dnpi = ( - frappe.qb.from_(PackedItem) - .select(PackedItem.qty, PackedItem.parent_detail_docname, PackedItem.parent, PackedItem.name) - .where(PackedItem.item_code == item_code) - .where(PackedItem.warehouse == warehouse) - ) - dnpi = append_open_so_query(dnpi, PackedItem) - - dnpi_parent = frappe.qb.from_(dnpi).select(dnpi.qty.as_("dnpi_qty")) - for key, so_item_field, cond in [ - ("so_item_qty", "qty", 1), - ("so_item_delivered_qty", "delivered_qty", 1), - ("so_item_returned_qty", "returned_qty", dont_reserve_qty_on_sales_return), - ]: - if cond: - dnpi_parent = dnpi_parent.select( - ( - frappe.qb.from_(SalesOrderItem) - .select(SalesOrderItem[so_item_field]) - .where(SalesOrderItem.name == dnpi.parent_detail_docname) - .where(SalesOrderItem.delivered_by_supplier == 0) - ).as_(key) - ) - dnpi_parent = dnpi_parent.select(dnpi.parent, dnpi.name) - - dnpi_parent = dnpi_parent + tab - - q = ( - frappe.qb.from_(dnpi_parent) - .select( - Sum( - dnpi_parent.dnpi_qty - * ( + reserved_qty = frappe.db.sql( + """ + select + sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty) / so_item_qty)) + from + ( + (select + qty as dnpi_qty, ( - dnpi_parent.so_item_qty - - dnpi_parent.so_item_delivered_qty - - (dnpi_parent.so_item_returned_qty if dont_reserve_qty_on_sales_return else 0) - ) - / dnpi_parent.so_item_qty - ) - ) - ) - .where(dnpi_parent.so_item_qty >= dnpi_parent.so_item_delivered_qty) + select qty from `tabSales Order Item` + where name = dnpi.parent_detail_docname + and (delivered_by_supplier is null or delivered_by_supplier = 0) + ) as so_item_qty, + ( + select delivered_qty from `tabSales Order Item` + where name = dnpi.parent_detail_docname + and delivered_by_supplier = 0 + ) as so_item_delivered_qty, + parent, name + from + ( + select qty, parent_detail_docname, parent, name + from `tabPacked Item` dnpi_in + where item_code = %s and warehouse = %s + and parenttype='Sales Order' + and item_code != parent_item + and exists (select * from `tabSales Order` so + where name = dnpi_in.parent and docstatus = 1 and status not in ('On Hold', 'Closed')) + ) dnpi) + union + (select stock_qty as dnpi_qty, qty as so_item_qty, + delivered_qty as so_item_delivered_qty, parent, name + from `tabSales Order Item` so_item + where item_code = %s and warehouse = %s + and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0) + and exists(select * from `tabSales Order` so + where so.name = so_item.parent and so.docstatus = 1 + and so.status not in ('On Hold', 'Closed'))) + ) tab + where + so_item_qty >= so_item_delivered_qty + """, + (item_code, warehouse, item_code, warehouse), ) - reserved_qty = q.run() return flt(reserved_qty[0][0]) if reserved_qty else 0 From 3c553b0938073ee3fb4a355c884812bf6ae136f3 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Mon, 27 Mar 2023 10:24:15 -0400 Subject: [PATCH 29/77] feat: don't reserve qty on sales return --- erpnext/stock/stock_balance.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 439ed7a8e0..e3cbb43d8b 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -94,10 +94,13 @@ def get_balance_qty_from_sle(item_code, warehouse): def get_reserved_qty(item_code, warehouse): + dont_reserve_on_return = frappe.get_cached_value( + "Selling Settings", "Selling Settings", "dont_reserve_sales_order_qty_on_sales_return" + ) reserved_qty = frappe.db.sql( - """ + f""" select - sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty) / so_item_qty)) + sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty - if(dont_reserve_qty_on_return, so_item_returned_qty, 0)) / so_item_qty)) from ( (select @@ -112,6 +115,12 @@ def get_reserved_qty(item_code, warehouse): where name = dnpi.parent_detail_docname and delivered_by_supplier = 0 ) as so_item_delivered_qty, + ( + select returned_qty from `tabSales Order Item` + where name = dnpi.parent_detail_docname + and delivered_by_supplier = 0 + ) as so_item_returned_qty, + {dont_reserve_on_return} as dont_reserve_qty_on_return, parent, name from ( @@ -125,7 +134,9 @@ def get_reserved_qty(item_code, warehouse): ) dnpi) union (select stock_qty as dnpi_qty, qty as so_item_qty, - delivered_qty as so_item_delivered_qty, parent, name + delivered_qty as so_item_delivered_qty, + returned_qty as so_item_returned_qty, + {dont_reserve_on_return}, parent, name from `tabSales Order Item` so_item where item_code = %s and warehouse = %s and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0) From d52f7e28202ec5359cf7ce03568df0bf39a06a54 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 28 Mar 2023 13:27:29 +0530 Subject: [PATCH 30/77] fix: removing redundant validation --- erpnext/accounts/utils.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 92906c189a..2ab9ef64b3 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -467,14 +467,6 @@ def reconcile_against_document(args): # nosemgrep else: update_reference_in_payment_entry(entry, doc, do_not_save=True) - if doc.doctype == "Journal Entry": - try: - doc.validate_total_debit_and_credit() - except Exception as validation_exception: - raise frappe.ValidationError( - _("Validation Error for {0}").format(doc.name) - ) from validation_exception - doc.save(ignore_permissions=True) # re-submit advance entry doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) From 12ad2aa2e5f5f173c9f52c07fb95e00b069dd403 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Mar 2023 15:33:59 +0530 Subject: [PATCH 31/77] fix: Percentage billing in Sales Order (#34606) --- erpnext/controllers/status_updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index dd2a67032f..58cab147a4 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -464,7 +464,7 @@ class StatusUpdater(Document): ifnull((select ifnull(sum(case when abs(%(target_ref_field)s) > abs(%(target_field)s) then abs(%(target_field)s) else abs(%(target_ref_field)s) end), 0) / sum(abs(%(target_ref_field)s)) * 100 - from `tab%(target_dt)s` where parent='%(name)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6) + from `tab%(target_dt)s` where parent='%(name)s' and parenttype='%(target_parent_dt)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6) %(update_modified)s where name='%(name)s'""" % args From 50c1172f29ac695ebb9b86f4947fb580380b58a6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Mar 2023 15:36:16 +0530 Subject: [PATCH 32/77] fix: Party Name in SOA print when viewed from Customer/Supplier master (#34597) fix: Party Name in SOA print when viewd from Customer/Supplier master --- erpnext/accounts/report/general_ledger/general_ledger.js | 5 ++--- erpnext/buying/doctype/supplier/supplier.js | 2 +- erpnext/selling/doctype/customer/customer.js | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 010284c2ea..2100f26c1e 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -58,9 +58,8 @@ frappe.query_reports["General Ledger"] = { { "fieldname":"party_type", "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - "default": "", + "fieldtype": "Autocomplete", + options: Object.keys(frappe.boot.party_account_types), on_change: function() { frappe.query_report.set_filter_value('party', ""); } diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index f0899b06b5..1ae6f03647 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -64,7 +64,7 @@ frappe.ui.form.on("Supplier", { // custom buttons frm.add_custom_button(__('Accounting Ledger'), function () { frappe.set_route('query-report', 'General Ledger', - { party_type: 'Supplier', party: frm.doc.name }); + { party_type: 'Supplier', party: frm.doc.name, party_name: frm.doc.supplier_name }); }, __("View")); frm.add_custom_button(__('Accounts Payable'), function () { diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 107e4a4759..b53f339229 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -123,7 +123,7 @@ frappe.ui.form.on("Customer", { frm.add_custom_button(__('Accounting Ledger'), function () { frappe.set_route('query-report', 'General Ledger', - {party_type: 'Customer', party: frm.doc.name}); + {party_type: 'Customer', party: frm.doc.name, party_name: frm.doc.customer_name}); }, __('View')); frm.add_custom_button(__('Pricing Rule'), function () { From 393bc25e2d98e1435dc3ac88d39907e647d56451 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Tue, 28 Mar 2023 07:04:24 -0400 Subject: [PATCH 33/77] fix: don't get zero value entries for exchange rate calculation (#34475) * fix: multiply None by float * chore: remove debug --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index a4f6a74a5a..81c2d8bb73 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -490,6 +490,8 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party): conditions.append(gl.company == company) conditions.append(gl.account == account) conditions.append(gl.is_cancelled == 0) + conditions.append((gl.debit > 0) | (gl.credit > 0)) + conditions.append((gl.debit_in_account_currency > 0) | (gl.credit_in_account_currency > 0)) if party_type: conditions.append(gl.party_type == party_type) if party: From 7aafc90d583d787da17ed9c70427d9d74f3fdaeb Mon Sep 17 00:00:00 2001 From: Vishal Dhayagude Date: Tue, 28 Mar 2023 16:48:03 +0530 Subject: [PATCH 34/77] fix: Tax Category not able to set hence it calculating zero tax for item whoes tax template set (#34525) * fix: Tax Category not able to set hence it calculating zero tax for item whoes tax template set * fix: minor change added --- erpnext/accounts/party.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b217f00065..ac9368e69c 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -174,6 +174,9 @@ def _get_party_details( party_type, party.name, "tax_withholding_category" ) + if not party_details.get("tax_category") and pos_profile: + party_details["tax_category"] = frappe.get_value("POS Profile", pos_profile, "tax_category") + return party_details From 07c9b990728ceba5b74bc81c4155e36bf19d9c77 Mon Sep 17 00:00:00 2001 From: akiratfli Date: Tue, 28 Mar 2023 19:50:54 +0800 Subject: [PATCH 35/77] fix: bad strings format for update-translations (#34592) Co-authored-by: justin.li --- .../doctype/bank_transaction/bank_transaction.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 15162376c1..fcbaf329f5 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -46,7 +46,7 @@ class BankTransaction(StatusUpdater): def add_payment_entries(self, vouchers): "Add the vouchers with zero allocation. Save() will perform the allocations and clearance" if 0.0 >= self.unallocated_amount: - frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled")) + frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name)) added = False for voucher in vouchers: @@ -114,9 +114,7 @@ class BankTransaction(StatusUpdater): elif 0.0 > unallocated_amount: self.db_delete_payment_entry(payment_entry) - frappe.throw( - frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}") - ) + frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount)) self.reload() @@ -178,7 +176,9 @@ def get_clearance_details(transaction, payment_entry): if gle["gl_account"] == gl_bank_account: if gle["amount"] <= 0.0: frappe.throw( - frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}") + frappe._("Voucher {0} value is broken: {1}").format( + payment_entry.payment_entry, gle["amount"] + ) ) unmatched_gles -= 1 From 867d8983046ad175700bc73a6b844320e3efb064 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 28 Mar 2023 18:45:16 +0530 Subject: [PATCH 36/77] fix: zero rm-cost for batch rm item in SCR (#34616) fix: `0` rm-cost for batch rm item in SCR --- erpnext/controllers/subcontracting_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index cc80f6ca98..05754293b7 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -455,7 +455,7 @@ class SubcontractingController(StockController): "allow_zero_valuation": 1, } ) - rm_obj.rate = get_incoming_rate(args) + rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args) if self.doctype == self.subcontract_data.order_doctype: rm_obj.required_qty = qty From 517b5f856728748461096c568e2f17633f068d75 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 29 Mar 2023 14:02:09 +0530 Subject: [PATCH 37/77] refactor: rewrite `batch.py` queries in `QB` --- erpnext/stock/doctype/batch/batch.py | 102 +++++++++++++-------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index f14288beb2..3876a2b145 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last +from frappe.query_builder.functions import CurDate, Sum, Timestamp from frappe.utils import cint, flt, get_link_to_form from frappe.utils.data import add_days from frappe.utils.jinja import render_template @@ -176,45 +177,38 @@ def get_batch_qty( :param warehouse: Optional - give qty for this warehouse :param item_code: Optional - give qty for this item""" + sle = frappe.qb.DocType("Stock Ledger Entry") + out = 0 if batch_no and warehouse: - cond = "" + query = ( + frappe.qb.from_(sle) + .select(Sum(sle.actual_qty)) + .where((sle.is_cancelled == 0) & (sle.warehouse == warehouse) & (sle.batch_no == batch_no)) + ) + if posting_date and posting_time: - cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format( - posting_date, posting_time + query = query.where( + Timestamp(sle.posting_date, sle.posting_time) <= Timestamp(posting_date, posting_time) ) - out = float( - frappe.db.sql( - """select sum(actual_qty) - from `tabStock Ledger Entry` - where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format( - cond - ), - (warehouse, batch_no), - )[0][0] - or 0 - ) + out = query.run(as_list=True)[0][0] or 0 if batch_no and not warehouse: - out = frappe.db.sql( - """select warehouse, sum(actual_qty) as qty - from `tabStock Ledger Entry` - where is_cancelled = 0 and batch_no=%s - group by warehouse""", - batch_no, - as_dict=1, - ) + out = ( + frappe.qb.from_(sle) + .select(sle.warehouse, Sum(sle.actual_qty).as_("qty")) + .where((sle.is_cancelled == 0) & (sle.batch_no == batch_no)) + .groupby(sle.warehouse) + ).run(as_dict=True) if not batch_no and item_code and warehouse: - out = frappe.db.sql( - """select batch_no, sum(actual_qty) as qty - from `tabStock Ledger Entry` - where is_cancelled = 0 and item_code = %s and warehouse=%s - group by batch_no""", - (item_code, warehouse), - as_dict=1, - ) + out = ( + frappe.qb.from_(sle) + .select(sle.batch_no, Sum(sle.actual_qty).as_("qty")) + .where((sle.is_cancelled == 0) & (sle.item_code == item_code) & (sle.warehouse == warehouse)) + .groupby(sle.batch_no) + ).run(as_dict=True) return out @@ -310,40 +304,44 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - cond = "" + batch = frappe.qb.DocType("Batch") + sle = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(batch) + .join(sle) + .on(batch.batch_id == sle.batch_no) + .select( + batch.batch_id, + Sum(sle.actual_qty).as_("qty"), + ) + .where( + (sle.item_code == item_code) + & (sle.warehouse == warehouse) + & (sle.is_cancelled == 0) + & ((batch.expiry_date >= CurDate()) | (batch.expiry_date.isnull())) + ) + .groupby(batch.batch_id) + .orderby(batch.expiry_date, batch.creation) + ) + if serial_no and frappe.get_cached_value("Item", item_code, "has_batch_no"): serial_nos = get_serial_nos(serial_no) - batch = frappe.get_all( + batches = frappe.get_all( "Serial No", fields=["distinct batch_no"], filters={"item_code": item_code, "warehouse": warehouse, "name": ("in", serial_nos)}, ) - if not batch: + if not batches: validate_serial_no_with_batch(serial_nos, item_code) - if batch and len(batch) > 1: + if batches and len(batches) > 1: return [] - cond = " and `tabBatch`.name = %s" % (frappe.db.escape(batch[0].batch_no)) + query = query.where(batch.name == batches[0].batch_no) - return frappe.db.sql( - """ - select batch_id, sum(`tabStock Ledger Entry`.actual_qty) as qty - from `tabBatch` - join `tabStock Ledger Entry` ignore index (item_code, warehouse) - on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no ) - where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s - and `tabStock Ledger Entry`.is_cancelled = 0 - and (`tabBatch`.expiry_date >= CURRENT_DATE or `tabBatch`.expiry_date IS NULL) {0} - group by batch_id - order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC - """.format( - cond - ), - (item_code, warehouse), - as_dict=True, - ) + return query.run(as_dict=True) def validate_serial_no_with_batch(serial_nos, item_code): From d0660ad222a5d5ef9196225a806f917d66984482 Mon Sep 17 00:00:00 2001 From: Komal-Saraf0609 <81952590+Komal-Saraf0609@users.noreply.github.com> Date: Wed, 29 Mar 2023 16:51:18 +0530 Subject: [PATCH 38/77] fix: lost opportunity report issue (#34626) * fix: lost opportunity report issue * chore: Linting Issues --------- Co-authored-by: Komal Saraf Co-authored-by: Deepesh Garg --- erpnext/crm/report/lead_details/lead_details.py | 2 +- erpnext/crm/report/lost_opportunity/lost_opportunity.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index 8660c73310..7b8c43b2d6 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -98,7 +98,7 @@ def get_data(filters): `tabAddress`.name=`tabDynamic Link`.parent) WHERE company = %(company)s - AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s + AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s {conditions} ORDER BY `tabLead`.creation asc """.format( diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py index 254511c92f..b37cfa449f 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -82,7 +82,7 @@ def get_data(filters): {join} WHERE `tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s - AND `tabOpportunity`.modified BETWEEN %(from_date)s AND %(to_date)s + AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s {conditions} GROUP BY `tabOpportunity`.name From f5453adf4d7240baa2b092aa9ec3265316a73b1f Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 29 Mar 2023 13:29:10 +0200 Subject: [PATCH 39/77] fix: switch to supported update_linked_doctypes (#34625) --- erpnext/selling/doctype/customer/customer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 944a0a6a76..ebafddb061 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -14,7 +14,7 @@ from frappe.contacts.address_and_contact import ( from frappe.desk.reportview import build_match_conditions, get_filters_cond from frappe.model.mapper import get_mapped_doc from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options -from frappe.model.rename_doc import update_linked_doctypes +from frappe.model.utils.rename_doc import update_linked_doctypes from frappe.utils import cint, cstr, flt, get_formatted_email, today from frappe.utils.user import get_users_with_role From ddc3050e99fffcadd1187d42b37bb66fb262a0a6 Mon Sep 17 00:00:00 2001 From: Bevan Tony Medrano Date: Wed, 29 Mar 2023 19:29:49 +0800 Subject: [PATCH 40/77] Asset maintenance task add dropdown "3 Yearly" (#34607) * feat(asset_maintenance.json):Add 3 yearly in periodicity dropdown * add server side implications for 3 yearly (cherry picked from commit 625b8e800537cceabffda50a0bbed1effc2e2aa5) --- .../asset_maintenance/asset_maintenance.py | 2 + .../asset_maintenance_task.json | 770 +++--------------- 2 files changed, 133 insertions(+), 639 deletions(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 0028d84508..83031415ec 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -84,6 +84,8 @@ def calculate_next_due_date( next_due_date = add_years(start_date, 1) if periodicity == "2 Yearly": next_due_date = add_years(start_date, 2) + if periodicity == "3 Yearly": + next_due_date = add_years(start_date, 3) if periodicity == "Quarterly": next_due_date = add_months(start_date, 3) if end_date and ( diff --git a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json index 20963e3fdc..b7cb23e668 100644 --- a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json +++ b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json @@ -1,664 +1,156 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2017-10-20 07:10:55.903571", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-10-20 07:10:55.903571", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "maintenance_task", + "maintenance_type", + "column_break_2", + "maintenance_status", + "section_break_2", + "start_date", + "periodicity", + "column_break_4", + "end_date", + "certificate_required", + "section_break_9", + "assign_to", + "column_break_10", + "assign_to_name", + "section_break_10", + "next_due_date", + "column_break_14", + "last_completion_date", + "section_break_7", + "description" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maintenance_task", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Maintenance Task", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "maintenance_task", + "fieldtype": "Data", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Maintenance Task", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maintenance_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Maintenance Type", - "length": 0, - "no_copy": 0, - "options": "Preventive Maintenance\nCalibration", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "maintenance_type", + "fieldtype": "Select", + "label": "Maintenance Type", + "options": "Preventive Maintenance\nCalibration" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "maintenance_status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Maintenance Status", - "length": 0, - "no_copy": 0, - "options": "Planned\nOverdue\nCancelled", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "maintenance_status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Maintenance Status", + "options": "Planned\nOverdue\nCancelled", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_2", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Start Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Today", + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "periodicity", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Periodicity", - "length": 0, - "no_copy": 0, - "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "periodicity", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Periodicity", + "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "End Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "certificate_required", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Certificate Required", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 1, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "certificate_required", + "fieldtype": "Check", + "label": "Certificate Required", + "search_index": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "assign_to", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Assign To", - "length": 0, - "no_copy": 0, - "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "assign_to", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Assign To", + "options": "User" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_10", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "assign_to.full_name", - "fieldname": "assign_to_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Assign to Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "assign_to_name", + "fieldtype": "Read Only", + "label": "Assign to Name" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_10", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "next_due_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Next Due Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "next_due_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Next Due Date" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_14", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "last_completion_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Last Completion Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "last_completion_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Last Completion Date" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-06-18 16:12:04.330021", - "modified_by": "Administrator", - "module": "Assets", - "name": "Asset Maintenance Task", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-03-23 07:03:07.113452", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Maintenance Task", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file From ad11934d39f0dca68630f4231031f8965fc74cf6 Mon Sep 17 00:00:00 2001 From: Komal-Saraf0609 <81952590+Komal-Saraf0609@users.noreply.github.com> Date: Thu, 30 Mar 2023 08:03:55 +0530 Subject: [PATCH 41/77] fix: enabling lead even after "Opportunity" created against it (#34627) * fix: enabling lead even after "Opportunity" created against it * chore: Linting Issues --------- Co-authored-by: Komal Saraf Co-authored-by: Deepesh Garg --- erpnext/crm/doctype/opportunity/opportunity.py | 5 ----- erpnext/patches.txt | 1 + erpnext/patches/v15_0/enable_all_leads.py | 8 ++++++++ 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 erpnext/patches/v15_0/enable_all_leads.py diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index f4b6e910ed..6a5fead0f8 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -33,7 +33,6 @@ class Opportunity(TransactionBase, CRMNote): def after_insert(self): if self.opportunity_from == "Lead": frappe.get_doc("Lead", self.party_name).set_status(update=True) - self.disable_lead() link_open_tasks(self.opportunity_from, self.party_name, self) link_open_events(self.opportunity_from, self.party_name, self) @@ -119,10 +118,6 @@ class Opportunity(TransactionBase, CRMNote): prospect.flags.ignore_mandatory = True prospect.save() - def disable_lead(self): - if self.opportunity_from == "Lead": - frappe.db.set_value("Lead", self.party_name, {"disabled": 1, "docstatus": 1}) - def make_new_lead_if_required(self): """Set lead against new opportunity""" if (not self.get("party_name")) and self.contact_email: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0ef51a951d..3357b06fbb 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -330,3 +330,4 @@ erpnext.patches.v14_0.update_closing_balances # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger execute:frappe.delete_doc_if_exists("Report", "Tax Detail") +erpnext.patches.v15_0.enable_all_leads diff --git a/erpnext/patches/v15_0/enable_all_leads.py b/erpnext/patches/v15_0/enable_all_leads.py new file mode 100644 index 0000000000..c1f2b47b5b --- /dev/null +++ b/erpnext/patches/v15_0/enable_all_leads.py @@ -0,0 +1,8 @@ +import frappe + + +def execute(): + lead = frappe.qb.DocType("Lead") + frappe.qb.update(lead).set(lead.disabled, 0).set(lead.docstatus, 0).where( + lead.disabled == 1 and lead.docstatus == 1 + ).run() From 17131e5a02ac51ea2a605a180571b1f31bf02110 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 30 Mar 2023 11:47:32 +0530 Subject: [PATCH 42/77] fix: serial no with zero quantity issue in stock reco --- .../stock_reconciliation/stock_reconciliation.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 3f6a2c881b..04d1a3a5e2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -4,7 +4,7 @@ from typing import Optional import frappe -from frappe import _, msgprint +from frappe import _, bold, msgprint from frappe.utils import cint, cstr, flt import erpnext @@ -89,7 +89,7 @@ class StockReconciliation(StockController): if item_dict.get("serial_nos"): item.current_serial_no = item_dict.get("serial_nos") - if self.purpose == "Stock Reconciliation" and not item.serial_no: + if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty: item.serial_no = item.current_serial_no item.current_qty = item_dict.get("qty") @@ -140,6 +140,14 @@ class StockReconciliation(StockController): self.validate_item(row.item_code, row) + if row.serial_no and not row.qty: + self.validation_messages.append( + _get_msg( + row_num, + f"Quantity should not be zero for the {bold(row.item_code)} since serial nos are specified", + ) + ) + # validate warehouse if not frappe.db.get_value("Warehouse", row.warehouse): self.validation_messages.append(_get_msg(row_num, _("Warehouse not found in the system"))) From 2b0470d1f5a69f07d2b2e0cad0f4b1b649608665 Mon Sep 17 00:00:00 2001 From: anandbaburajan Date: Thu, 30 Mar 2023 12:50:33 +0530 Subject: [PATCH 43/77] fix: incorrect arg name in asset value adjustment --- .../doctype/asset_value_adjustment/asset_value_adjustment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js index ae0e1bda02..d07f40cdf4 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js @@ -49,7 +49,7 @@ frappe.ui.form.on('Asset Value Adjustment', { frm.call({ method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation", args: { - asset: frm.doc.asset, + asset_name: frm.doc.asset, finance_book: frm.doc.finance_book }, callback: function(r) { From a4112c75c5975b53e46ea5bab47daf1c4d8d7e7e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 30 Mar 2023 13:46:50 +0530 Subject: [PATCH 44/77] fix: BOM Update Cost, when no actual qty --- erpnext/manufacturing/doctype/bom/bom.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 619a415c8b..a085af859a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -943,7 +943,8 @@ def get_valuation_rate(data): 2) If no value, get last valuation rate from SLE 3) If no value, get valuation rate from Item """ - from frappe.query_builder.functions import Sum + from frappe.query_builder.functions import Count, IfNull, Sum + from pypika import Case item_code, company = data.get("item_code"), data.get("company") valuation_rate = 0.0 @@ -954,7 +955,14 @@ def get_valuation_rate(data): frappe.qb.from_(bin_table) .join(wh_table) .on(bin_table.warehouse == wh_table.name) - .select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate")) + .select( + Case() + .when( + Count(bin_table.name) > 0, IfNull(Sum(bin_table.stock_value) / Sum(bin_table.actual_qty), 0.0) + ) + .else_(None) + .as_("valuation_rate") + ) .where((bin_table.item_code == item_code) & (wh_table.company == company)) ).run(as_dict=True)[0] From af3e807607ccb994e45919f967d4955e011a7915 Mon Sep 17 00:00:00 2001 From: anandbaburajan Date: Thu, 30 Mar 2023 15:28:59 +0530 Subject: [PATCH 45/77] chore: auto fill asset name and available for use date --- erpnext/assets/doctype/asset/asset.js | 3 +++ erpnext/assets/doctype/asset/asset.json | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 4951385136..b9f16a795a 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -466,6 +466,9 @@ frappe.ui.form.on('Asset', { } else { frm.set_value('purchase_date', purchase_doc.posting_date); } + if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) { + frm.set_value('available_for_use_date', frm.doc.purchase_date); + } const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code); if (!item) { doctype_field = frappe.scrub(doctype) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index ea575fd71f..a1e8f331cd 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -79,6 +79,9 @@ "options": "ACC-ASS-.YYYY.-" }, { + "depends_on": "item_code", + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, "fieldname": "asset_name", "fieldtype": "Data", "in_list_view": 1, @@ -517,7 +520,7 @@ "table_fieldname": "accounts" } ], - "modified": "2023-02-02 00:03:11.706427", + "modified": "2023-03-30 15:07:41.542374", "modified_by": "Administrator", "module": "Assets", "name": "Asset", From cbdaab940d053268bc2592b3bf56c1b8ab389dde Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 30 Mar 2023 16:20:42 +0530 Subject: [PATCH 46/77] fix: incorrect balance qty in the stock ledger report --- erpnext/stock/report/stock_ledger/stock_ledger.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index da17cdeb5a..77bc4e004d 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -34,6 +34,9 @@ def execute(filters=None): conversion_factors.append(0) actual_qty = stock_value = 0 + if opening_row: + actual_qty = opening_row.get("qty_after_transaction") + stock_value = opening_row.get("stock_value") available_serial_nos = {} inventory_dimension_filters_applied = check_inventory_dimension_filters_applied(filters) From 345e6facbee4e874b17559be75498b47fece1d1f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 29 Mar 2023 15:31:24 +0530 Subject: [PATCH 47/77] fix: posting time issue --- erpnext/stock/doctype/batch/batch.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index f14288beb2..4a165212dc 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -6,7 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last -from frappe.utils import cint, flt, get_link_to_form +from frappe.utils import cint, flt, get_link_to_form, nowtime from frappe.utils.data import add_days from frappe.utils.jinja import render_template @@ -179,7 +179,11 @@ def get_batch_qty( out = 0 if batch_no and warehouse: cond = "" - if posting_date and posting_time: + + if posting_date: + if posting_time is None: + posting_time = nowtime() + cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format( posting_date, posting_time ) From d999dea3e4f7397e6d649e15104199331db3bd45 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Thu, 30 Mar 2023 16:38:37 +0530 Subject: [PATCH 48/77] chore: improve asset depr posting failure msg (#34661) * chore: improve asset depr posting error msg * chore: add period * chore: improve msg --- erpnext/assets/doctype/asset/depreciation.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index fb6e174fba..028e3d6268 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -249,10 +249,16 @@ def notify_depr_entry_posting_error(failed_asset_names): asset_links = get_comma_separated_asset_links(failed_asset_names) message = ( - _("Hi,") - + "
" - + _("The following assets have failed to post depreciation entries: {0}").format(asset_links) + _("Hello,") + + "

" + + _("The following assets have failed to automatically post depreciation entries: {0}").format( + asset_links + ) + "." + + "

" + + _( + "Please raise a support ticket and share this email, or forward this email to your development team so that they can find the issue in the developer console by manually creating the depreciation entry via the asset's depreciation schedule table." + ) ) frappe.sendmail(recipients=recipients, subject=subject, message=message) From ddb17a888076975bb7af020ace98acc4d9caaf23 Mon Sep 17 00:00:00 2001 From: Richard Case <110036763+casesolved-co-uk@users.noreply.github.com> Date: Thu, 30 Mar 2023 12:15:56 +0100 Subject: [PATCH 49/77] fix: plaid log_error syntax issue (#34642) --- .../doctype/plaid_settings/plaid_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index f3aa6a3793..e57a30a88e 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -220,7 +220,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None): if e.code == "ITEM_LOGIN_REQUIRED": msg = _("There was an error syncing transactions.") + " " msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " " - frappe.log_error(msg, title=_("Plaid Link Refresh Required")) + frappe.log_error(message=msg, title=_("Plaid Link Refresh Required")) return transactions From 7c42b72ee7882fc7963187e036c770a78e72cab1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 30 Mar 2023 17:28:19 +0530 Subject: [PATCH 50/77] fix: Total debit and credit while importing via Data Import (#34659) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index db399b7bad..68364beba2 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -51,7 +51,7 @@ class JournalEntry(AccountsController): self.validate_multi_currency() self.set_amounts_in_company_currency() self.validate_debit_credit_amount() - + self.set_total_debit_credit() # Do not validate while importing via data import if not frappe.flags.in_import: self.validate_total_debit_and_credit() @@ -666,7 +666,6 @@ class JournalEntry(AccountsController): frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx)) def validate_total_debit_and_credit(self): - self.set_total_debit_credit() if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency): if self.difference: frappe.throw( From 706be2a4155209abd3065fa1225f441b3c759740 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 31 Mar 2023 10:32:49 +0530 Subject: [PATCH 51/77] chore: make `Production Plan Item Reference` table hidden in Production Plan --- .../doctype/production_plan/production_plan.json | 3 ++- .../production_plan_item_reference.json | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 2624daa41e..fdaa4a2a1d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -344,6 +344,7 @@ { "fieldname": "prod_plan_references", "fieldtype": "Table", + "hidden": 1, "label": "Production Plan Item Reference", "options": "Production Plan Item Reference" }, @@ -397,7 +398,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-11-26 14:51:08.774372", + "modified": "2023-03-31 10:30:48.118932", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json index 84dee4ad28..15ef20794c 100644 --- a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json +++ b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json @@ -28,7 +28,7 @@ "fieldname": "qty", "fieldtype": "Data", "in_list_view": 1, - "label": "qty" + "label": "Qty" }, { "fieldname": "item_reference", @@ -40,7 +40,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-07 17:03:49.707487", + "modified": "2023-03-31 10:30:14.604051", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Item Reference", @@ -48,5 +48,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 576575c22798a011a092196fdd8c7523d238c682 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 31 Mar 2023 11:10:50 +0530 Subject: [PATCH 52/77] fix: Column value mismatch in COA blank template (#34658) --- .../chart_of_accounts_importer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index cb7da17901..d6e1be4123 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -325,14 +325,14 @@ def get_template(template_type): if template_type == "Blank Template": for root_type in get_root_types(): - writer.writerow(["", "", "", 1, "", root_type]) + writer.writerow(["", "", "", "", 1, "", root_type]) for account in get_mandatory_group_accounts(): - writer.writerow(["", "", "", 1, account, "Asset"]) + writer.writerow(["", "", "", "", 1, account, "Asset"]) for account_type in get_mandatory_account_types(): writer.writerow( - ["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")] + ["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")] ) else: writer = get_sample_template(writer) From 986daa65784fde3a2334599b58be222f030d79f6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 31 Mar 2023 12:09:57 +0530 Subject: [PATCH 53/77] fix: enclose ternary operator in parentheses --- erpnext/public/js/controllers/taxes_and_totals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 8e57ebd367..8efc47d18e 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -135,7 +135,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } else { // allow for '0' qty on Credit/Debit notes - let qty = item.qty || me.frm.doc.is_debit_note ? 1 : -1; + let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1); item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); } From 32a4ca6b6c65939e9d2db8b281bbc62cc886b883 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 31 Mar 2023 14:57:49 +0530 Subject: [PATCH 54/77] fix: recalculate difference amount on allocation change --- .../payment_reconciliation.js | 28 +++++++++++++++++++ .../payment_reconciliation.py | 9 ++++++ 2 files changed, 37 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index d986f32066..caffac5354 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -272,4 +272,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo } }; +frappe.ui.form.on('Payment Reconciliation Allocation', { + allocated_amount: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + // filter invoice + let invoice = frm.doc.invoices.filter((x) => (x.invoice_number == row.invoice_number)); + // filter payment + let payment = frm.doc.payments.filter((x) => (x.reference_name == row.reference_name)); + + frm.call({ + doc: frm.doc, + method: 'calculate_difference_on_allocation_change', + args: { + payment_entry: payment, + invoice: invoice, + allocated_amount: row.allocated_amount + }, + callback: (r) => { + if (r.message) { + row.difference_amount = r.message; + frm.refresh(); + } + } + }); + } +}); + + + extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm})); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index c9e3998ac8..d8082d058f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -233,6 +233,15 @@ class PaymentReconciliation(Document): return difference_amount + @frappe.whitelist() + def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount): + invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry) + invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number")) + new_difference_amount = self.get_difference_amount( + payment_entry[0], invoice[0], allocated_amount + ) + return new_difference_amount + @frappe.whitelist() def allocate_entries(self, args): self.validate_entries() From 74b29eb5e22c15a8055322f4143c9f401cabaa7f Mon Sep 17 00:00:00 2001 From: "Kitti U. @ Ecosoft" Date: Sat, 1 Apr 2023 20:20:30 +0700 Subject: [PATCH 55/77] fix: Bank clearance for case loan (disburstment/repayment) (#34586) --- .../doctype/bank_clearance/bank_clearance.py | 16 +++-- .../bank_clearance_summary.py | 64 ++++++++++++++++++- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 80878ac506..081718726b 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -81,7 +81,7 @@ class BankClearance(Document): loan_disbursement = frappe.qb.DocType("Loan Disbursement") - loan_disbursements = ( + query = ( frappe.qb.from_(loan_disbursement) .select( ConstantColumn("Loan Disbursement").as_("payment_document"), @@ -90,17 +90,22 @@ class BankClearance(Document): ConstantColumn(0).as_("debit"), loan_disbursement.reference_number.as_("cheque_number"), loan_disbursement.reference_date.as_("cheque_date"), + loan_disbursement.clearance_date.as_("clearance_date"), loan_disbursement.disbursement_date.as_("posting_date"), loan_disbursement.applicant.as_("against_account"), ) .where(loan_disbursement.docstatus == 1) .where(loan_disbursement.disbursement_date >= self.from_date) .where(loan_disbursement.disbursement_date <= self.to_date) - .where(loan_disbursement.clearance_date.isnull()) .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account])) .orderby(loan_disbursement.disbursement_date) .orderby(loan_disbursement.name, order=frappe.qb.desc) - ).run(as_dict=1) + ) + + if not self.include_reconciled_entries: + query = query.where(loan_disbursement.clearance_date.isnull()) + + loan_disbursements = query.run(as_dict=1) loan_repayment = frappe.qb.DocType("Loan Repayment") @@ -113,16 +118,19 @@ class BankClearance(Document): ConstantColumn(0).as_("credit"), loan_repayment.reference_number.as_("cheque_number"), loan_repayment.reference_date.as_("cheque_date"), + loan_repayment.clearance_date.as_("clearance_date"), loan_repayment.applicant.as_("against_account"), loan_repayment.posting_date, ) .where(loan_repayment.docstatus == 1) - .where(loan_repayment.clearance_date.isnull()) .where(loan_repayment.posting_date >= self.from_date) .where(loan_repayment.posting_date <= self.to_date) .where(loan_repayment.payment_account.isin([self.bank_account, self.account])) ) + if not self.include_reconciled_entries: + query = query.where(loan_repayment.clearance_date.isnull()) + if frappe.db.has_column("Loan Repayment", "repay_from_salary"): query = query.where((loan_repayment.repay_from_salary == 0)) diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index 449ebdcd92..306af722ba 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.custom import ConstantColumn from frappe.utils import getdate, nowdate @@ -91,4 +92,65 @@ def get_entries(filters): as_list=1, ) - return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate())) + # Loan Disbursement + loan_disbursement = frappe.qb.DocType("Loan Disbursement") + + query = ( + frappe.qb.from_(loan_disbursement) + .select( + ConstantColumn("Loan Disbursement").as_("payment_document_type"), + loan_disbursement.name.as_("payment_entry"), + loan_disbursement.disbursement_date.as_("posting_date"), + loan_disbursement.reference_number.as_("cheque_no"), + loan_disbursement.clearance_date.as_("clearance_date"), + loan_disbursement.applicant.as_("against"), + -loan_disbursement.disbursed_amount.as_("amount"), + ) + .where(loan_disbursement.docstatus == 1) + .where(loan_disbursement.disbursement_date >= filters["from_date"]) + .where(loan_disbursement.disbursement_date <= filters["to_date"]) + .where(loan_disbursement.disbursement_account == filters["account"]) + .orderby(loan_disbursement.disbursement_date, order=frappe.qb.desc) + .orderby(loan_disbursement.name, order=frappe.qb.desc) + ) + + if filters.get("from_date"): + query = query.where(loan_disbursement.disbursement_date >= filters["from_date"]) + if filters.get("to_date"): + query = query.where(loan_disbursement.disbursement_date <= filters["to_date"]) + + loan_disbursements = query.run(as_list=1) + + # Loan Repayment + loan_repayment = frappe.qb.DocType("Loan Repayment") + + query = ( + frappe.qb.from_(loan_repayment) + .select( + ConstantColumn("Loan Repayment").as_("payment_document_type"), + loan_repayment.name.as_("payment_entry"), + loan_repayment.posting_date.as_("posting_date"), + loan_repayment.reference_number.as_("cheque_no"), + loan_repayment.clearance_date.as_("clearance_date"), + loan_repayment.applicant.as_("against"), + loan_repayment.amount_paid.as_("amount"), + ) + .where(loan_repayment.docstatus == 1) + .where(loan_repayment.posting_date >= filters["from_date"]) + .where(loan_repayment.posting_date <= filters["to_date"]) + .where(loan_repayment.payment_account == filters["account"]) + .orderby(loan_repayment.posting_date, order=frappe.qb.desc) + .orderby(loan_repayment.name, order=frappe.qb.desc) + ) + + if filters.get("from_date"): + query = query.where(loan_repayment.posting_date >= filters["from_date"]) + if filters.get("to_date"): + query = query.where(loan_repayment.posting_date <= filters["to_date"]) + + loan_repayments = query.run(as_list=1) + + return sorted( + journal_entries + payment_entries + loan_disbursements + loan_repayments, + key=lambda k: k[2] or getdate(nowdate()), + ) From 4c61ee30bbfb56aec18d3cac5770d786f635931b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 2 Apr 2023 09:35:27 +0530 Subject: [PATCH 56/77] fix: Multiple issues in purchase invoice submission (#34600) * fix: Multiple issues in purchase invoice submission * fix: Base grand total calculation * chore: Calculate base grand total separately only in multi currency docs * fix: Add gl entry for round off --- .../purchase_invoice/purchase_invoice.py | 27 +++++++++- .../doctype/sales_invoice/sales_invoice.py | 2 +- erpnext/controllers/accounts_controller.py | 39 ++++++++++++--- erpnext/stock/get_item_details.py | 23 +++++++-- erpnext/utilities/transaction_base.py | 49 ++++++++++++++----- 5 files changed, 114 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b79af71bef..a617447856 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -117,7 +117,7 @@ class PurchaseInvoice(BuyingController): self.validate_expense_account() self.set_against_expense_account() self.validate_write_off_account() - self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items") + self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount") self.create_remarks() self.set_status() self.validate_purchase_receipt_if_update_stock() @@ -232,7 +232,7 @@ class PurchaseInvoice(BuyingController): ) if ( - cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) + cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate")) and not self.is_return and not self.is_internal_supplier ): @@ -581,6 +581,7 @@ class PurchaseInvoice(BuyingController): self.make_supplier_gl_entry(gl_entries) self.make_item_gl_entries(gl_entries) + self.make_precision_loss_gl_entry(gl_entries) if self.check_asset_cwip_enabled(): self.get_asset_gl_entry(gl_entries) @@ -975,6 +976,28 @@ class PurchaseInvoice(BuyingController): item.item_tax_amount, item.precision("item_tax_amount") ) + def make_precision_loss_gl_entry(self, gl_entries): + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Purchase Invoice", self.name + ) + + precision_loss = self.get("base_net_total") - flt( + self.get("net_total") * self.conversion_rate, self.precision("net_total") + ) + + if precision_loss: + gl_entries.append( + self.get_gl_dict( + { + "account": round_off_account, + "against": self.supplier, + "credit": precision_loss, + "cost_center": self.cost_center or round_off_cost_center, + "remarks": _("Net total calculation precision loss"), + } + ) + ) + def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 5cda276087..db619950e1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -145,7 +145,7 @@ class SalesInvoice(SellingController): self.set_against_income_account() self.validate_time_sheets_are_submitted() - self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items") + self.validate_multiple_billing("Delivery Note", "dn_detail", "amount") if not self.is_return: self.validate_serial_numbers() else: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3705fcf499..390af0deb2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -515,6 +515,8 @@ class AccountsController(TransactionBase): parent_dict.update({"customer": parent_dict.get("party_name")}) self.pricing_rules = [] + basic_item_details_map = {} + for item in self.get("items"): if item.get("item_code"): args = parent_dict.copy() @@ -533,7 +535,17 @@ class AccountsController(TransactionBase): if self.get("is_subcontracted"): args["is_subcontracted"] = self.is_subcontracted - ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False) + basic_details = basic_item_details_map.get(item.item_code) + ret, basic_item_details = get_item_details( + args, + self, + for_validate=True, + overwrite_warehouse=False, + return_basic_details=True, + basic_details=basic_details, + ) + + basic_item_details_map.setdefault(item.item_code, basic_item_details) for fieldname, value in ret.items(): if item.meta.get_field(fieldname) and value is not None: @@ -1232,7 +1244,7 @@ class AccountsController(TransactionBase): ) ) - def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield): + def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on): from erpnext.controllers.status_updater import get_allowance_for item_allowance = {} @@ -1245,17 +1257,20 @@ class AccountsController(TransactionBase): total_overbilled_amt = 0.0 + reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)] + reference_details = self.get_billing_reference_details( + reference_names, ref_dt + " Item", based_on + ) + for item in self.get("items"): if not item.get(item_ref_dn): continue - ref_amt = flt( - frappe.db.get_value(ref_dt + " Item", item.get(item_ref_dn), based_on), - self.precision(based_on, item), - ) + ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item)) + if not ref_amt: frappe.msgprint( - _("System will not check overbilling since amount for Item {0} in {1} is zero").format( + _("System will not check over billing since amount for Item {0} in {1} is zero").format( item.item_code, ref_dt ), title=_("Warning"), @@ -1302,6 +1317,16 @@ class AccountsController(TransactionBase): alert=True, ) + def get_billing_reference_details(self, reference_names, reference_doctype, based_on): + return frappe._dict( + frappe.get_all( + reference_doctype, + filters={"name": ("in", reference_names)}, + fields=["name", based_on], + as_list=1, + ) + ) + def get_billed_amount_for_item(self, item, item_ref_dn, based_on): """ Returns Sum of Amount of diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 489ec6ebec..2df39c8183 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -35,7 +35,14 @@ purchase_doctypes = [ @frappe.whitelist() -def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True): +def get_item_details( + args, + doc=None, + for_validate=False, + overwrite_warehouse=True, + return_basic_details=False, + basic_details=None, +): """ args = { "item_code": "", @@ -73,7 +80,13 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if doc.get("doctype") == "Purchase Invoice": args["bill_date"] = doc.get("bill_date") - out = get_basic_details(args, item, overwrite_warehouse) + if not basic_details: + out = get_basic_details(args, item, overwrite_warehouse) + else: + out = basic_details + + basic_details = out.copy() + get_item_tax_template(args, item, out) out["item_tax_rate"] = get_item_tax_map( args.company, @@ -141,7 +154,11 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru out.amount = flt(args.qty) * flt(out.rate) out = remove_standard_fields(out) - return out + + if return_basic_details: + return out, basic_details + else: + return out def remove_standard_fields(details): diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 21a0a551b6..fc2054533d 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -58,11 +58,11 @@ class TransactionBase(StatusUpdater): def compare_values(self, ref_doc, fields, doc=None): for reference_doctype, ref_dn_list in ref_doc.items(): + prev_doc_detail_map = self.get_prev_doc_reference_details( + ref_dn_list, reference_doctype, fields + ) for reference_name in ref_dn_list: - prevdoc_values = frappe.db.get_value( - reference_doctype, reference_name, [d[0] for d in fields], as_dict=1 - ) - + prevdoc_values = prev_doc_detail_map.get(reference_name) if not prevdoc_values: frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name)) @@ -70,6 +70,19 @@ class TransactionBase(StatusUpdater): if prevdoc_values[field] is not None and field not in self.exclude_fields: self.validate_value(field, condition, prevdoc_values[field], doc) + def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields): + prev_doc_detail_map = {} + details = frappe.get_all( + reference_doctype, + filters={"name": ("in", reference_names)}, + fields=["name"] + [d[0] for d in fields], + ) + + for d in details: + prev_doc_detail_map.setdefault(d.name, d) + + return prev_doc_detail_map + def validate_rate_with_reference_doc(self, ref_details): if self.get("is_internal_supplier"): return @@ -77,23 +90,23 @@ class TransactionBase(StatusUpdater): buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"] if self.doctype in buying_doctypes: - action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action") - settings_doc = "Buying Settings" + action, role_allowed_to_override = frappe.get_cached_value( + "Buying Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"] + ) else: - action = frappe.db.get_single_value("Selling Settings", "maintain_same_rate_action") - settings_doc = "Selling Settings" + action, role_allowed_to_override = frappe.get_cached_value( + "Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"] + ) for ref_dt, ref_dn_field, ref_link_field in ref_details: + reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)] + reference_details = self.get_reference_details(reference_names, ref_dt + " Item") for d in self.get("items"): if d.get(ref_link_field): - ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate") + ref_rate = reference_details.get(d.get(ref_link_field)) if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01: if action == "Stop": - role_allowed_to_override = frappe.db.get_single_value( - settings_doc, "role_to_override_stop_action" - ) - if role_allowed_to_override not in frappe.get_roles(): frappe.throw( _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( @@ -109,6 +122,16 @@ class TransactionBase(StatusUpdater): indicator="orange", ) + def get_reference_details(self, reference_names, reference_doctype): + return frappe._dict( + frappe.get_all( + reference_doctype, + filters={"name": ("in", reference_names)}, + fields=["name", "rate"], + as_list=1, + ) + ) + def get_link_filters(self, for_doctype): if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype): fieldname = self.prev_link_mapper[for_doctype]["fieldname"] From 6ec7590c21d3f97044ed13799ea9c85beeb4cf23 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 3 Apr 2023 14:06:07 +0530 Subject: [PATCH 57/77] fix: consider qty field precision --- erpnext/utilities/transaction_base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 21a0a551b6..d84e4c463c 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -186,12 +186,15 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): for f in qty_fields: qty = d.get(f) if qty: - if abs(cint(qty) - flt(qty)) > 0.0000001: + if abs(cint(qty) - flt(qty, d.precision(f))) > 0.0000001: frappe.throw( _( "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." ).format( - qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field)) + flt(qty, d.precision(f)), + d.idx, + frappe.bold(_("Must be Whole Number")), + frappe.bold(d.get(uom_field)), ), UOMMustBeIntegerError, ) From d56070301cedc3ffbafa8e7c556e775253fddd77 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 3 Apr 2023 14:47:58 +0530 Subject: [PATCH 58/77] fix: bom update log not working for large batch size --- .../manufacturing/doctype/bom_update_log/bom_update_log.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 51f7b24e74..7477f9528e 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -164,7 +164,7 @@ def queue_bom_cost_jobs( while current_boms_list: batch_no += 1 - batch_size = 20_000 + batch_size = 7_000 boms_to_process = current_boms_list[:batch_size] # slice out batch of 20k BOMs # update list to exclude 20K (queued) BOMs @@ -212,7 +212,7 @@ def resume_bom_cost_update_jobs(): ["name", "boms_updated", "status"], ) incomplete_level = any(row.get("status") == "Pending" for row in bom_batches) - if not bom_batches or not incomplete_level: + if not bom_batches or incomplete_level: continue # Prep parent BOMs & updated processed BOMs for next level @@ -252,9 +252,6 @@ def get_processed_current_boms( current_boms = [] for row in bom_batches: - if not row.boms_updated: - continue - boms_updated = json.loads(row.boms_updated) current_boms.extend(boms_updated) boms_updated_dict = {bom: True for bom in boms_updated} From b70615ef18eb08a0ddba7fa14e6cb043219aa8ee Mon Sep 17 00:00:00 2001 From: Hossein Yousefian <86075967+ihosseinu@users.noreply.github.com> Date: Tue, 4 Apr 2023 14:49:43 +0330 Subject: [PATCH 59/77] 'Make Asset Movement' button translation fix --- erpnext/assets/doctype/asset/asset_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 3d00eb74aa..5f53b005aa 100644 --- a/erpnext/assets/doctype/asset/asset_list.js +++ b/erpnext/assets/doctype/asset/asset_list.js @@ -36,7 +36,7 @@ frappe.listview_settings['Asset'] = { } }, onload: function(me) { - me.page.add_action_item('Make Asset Movement', function() { + me.page.add_action_item(__("Make Asset Movement"), function() { const assets = me.get_checked_items(); frappe.call({ method: "erpnext.assets.doctype.asset.asset.make_asset_movement", From 9c66ebb10846b1a7b5e38ef63d03c80807e2444c Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 4 Apr 2023 18:02:55 +0530 Subject: [PATCH 60/77] chore!: remove override to set item tax based on HSN code --- erpnext/stock/doctype/item/item.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 05a37ee4c4..3f0ac3e2ba 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -117,7 +117,6 @@ class Item(Document): self.validate_auto_reorder_enabled_in_stock_settings() self.cant_change() self.validate_item_tax_net_rate_range() - set_item_tax_from_hsn_code(self) if not self.is_new(): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") @@ -1316,11 +1315,6 @@ def update_variants(variants, template, publish_progress=True): frappe.publish_progress(count / total * 100, title=_("Updating Variants...")) -@erpnext.allow_regional -def set_item_tax_from_hsn_code(item): - pass - - def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None: for item_default in item_defaults: for doctype, field in [ From ef4bd771968274d73ec5df865159d251c91ebb3e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 4 Apr 2023 23:56:57 +0530 Subject: [PATCH 61/77] fix: incorrect stock balance quantity for batch item --- .../stock_reconciliation.py | 49 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 15 +++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 04d1a3a5e2..482b103d1e 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -5,6 +5,7 @@ from typing import Optional import frappe from frappe import _, bold, msgprint +from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt import erpnext @@ -569,6 +570,54 @@ class StockReconciliation(StockController): else: self._cancel() + def recalculate_current_qty(self, item_code, batch_no): + for row in self.items: + if not (row.item_code == item_code and row.batch_no == batch_no): + continue + + row.current_qty = get_batch_qty_for_stock_reco(item_code, row.warehouse, batch_no) + + qty, val_rate = get_stock_balance( + item_code, + row.warehouse, + self.posting_date, + self.posting_time, + with_valuation_rate=True, + ) + + row.current_valuation_rate = val_rate + + row.db_set( + { + "current_qty": row.current_qty, + "current_valuation_rate": row.current_valuation_rate, + "current_amount": flt(row.current_qty * row.current_valuation_rate), + } + ) + + +def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no): + ledger = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(ledger) + .select( + Sum(ledger.actual_qty).as_("batch_qty"), + ) + .where( + (ledger.item_code == item_code) + & (ledger.warehouse == warehouse) + & (ledger.docstatus == 1) + & (ledger.is_cancelled == 0) + & (ledger.batch_no == batch_no) + ) + .groupby(ledger.batch_no) + ) + + sle = query.run(as_dict=True) + + return flt(sle[0].batch_qty) if sle else 0 + @frappe.whitelist() def get_items( diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 08fc6fbd42..c954befdc2 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1337,6 +1337,9 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): next_stock_reco_detail = get_next_stock_reco(args) if next_stock_reco_detail: detail = next_stock_reco_detail[0] + if detail.batch_no: + regenerate_sle_for_batch_stock_reco(detail) + # add condition to update SLEs before this date & time datetime_limit_condition = get_datetime_limit_condition(detail) @@ -1364,6 +1367,16 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): validate_negative_qty_in_future_sle(args, allow_negative_stock) +def regenerate_sle_for_batch_stock_reco(detail): + doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no) + doc.docstatus = 2 + doc.update_stock_ledger() + + doc.recalculate_current_qty(detail.item_code, detail.batch_no) + doc.docstatus = 1 + doc.update_stock_ledger() + + def get_stock_reco_qty_shift(args): stock_reco_qty_shift = 0 if args.get("is_cancelled"): @@ -1393,7 +1406,7 @@ def get_next_stock_reco(args): return frappe.db.sql( """ select - name, posting_date, posting_time, creation, voucher_no + name, posting_date, posting_time, creation, voucher_no, item_code, batch_no, actual_qty from `tabStock Ledger Entry` where From 1101b7bfdfebe01ddcca5ccd48b455e389e2048d Mon Sep 17 00:00:00 2001 From: anandbaburajan Date: Tue, 4 Apr 2023 17:49:16 +0530 Subject: [PATCH 62/77] fix: don't include cancelled JVs in assdeprledger report (cherry picked from commit 3896d41e95a2054b4ec76c5f5b2819b3f28ef98e) --- .../asset_depreciation_ledger/asset_depreciation_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index 57d80492ae..f21c94b494 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -25,6 +25,7 @@ def get_data(filters): ["posting_date", "<=", filters.get("to_date")], ["against_voucher_type", "=", "Asset"], ["account", "in", depreciation_accounts], + ["is_cancelled", "=", 0], ] if filters.get("asset"): From 5cc3c080590845fe8199641912961f548b151970 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 5 Apr 2023 11:46:31 +0530 Subject: [PATCH 63/77] fix: asset monthly WDV and DD schedule and refactor [develop] (#34646) * fix: monthly wdv and dd schedule * chore: minor rename * chore: fix tests --- erpnext/assets/doctype/asset/asset.py | 86 ++----- erpnext/assets/doctype/asset/test_asset.py | 25 ++- .../asset_depreciation_schedule.py | 209 ++++++++++++++++-- 3 files changed, 221 insertions(+), 99 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index b5b7ba88f5..6001254762 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -8,16 +8,12 @@ import math import frappe from frappe import _ from frappe.utils import ( - add_months, cint, - date_diff, flt, get_datetime, get_last_day, get_link_to_form, getdate, - is_last_day_of_the_month, - month_diff, nowdate, today, ) @@ -239,30 +235,6 @@ class Asset(AccountsController): self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") ) - # if it returns True, depreciation_amount will not be equal for the first and last rows - def check_is_pro_rata(self, row): - has_pro_rata = False - - # if not existing asset, from_date = available_for_use_date - # otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 - # from_date = 01/01/2022 - from_date = self.get_modified_available_for_use_date(row) - days = date_diff(row.depreciation_start_date, from_date) + 1 - - # if frequency_of_depreciation is 12 months, total_days = 365 - total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) - - if days < total_days: - has_pro_rata = True - - return has_pro_rata - - def get_modified_available_for_use_date(self, row): - return add_months( - self.available_for_use_date, - (self.number_of_depreciations_booked * row.frequency_of_depreciation), - ) - def validate_asset_finance_books(self, row): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw( @@ -471,29 +443,6 @@ class Asset(AccountsController): return records - @erpnext.allow_regional - def get_depreciation_amount(self, depreciable_value, fb_row): - if fb_row.depreciation_method in ("Straight Line", "Manual"): - # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value - if self.flags.increase_in_asset_life: - depreciation_amount = ( - flt(fb_row.value_after_depreciation) - flt(fb_row.expected_value_after_useful_life) - ) / (date_diff(self.to_date, self.available_for_use_date) / 365) - # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value - elif self.flags.increase_in_asset_value_due_to_repair: - depreciation_amount = ( - flt(fb_row.value_after_depreciation) - flt(fb_row.expected_value_after_useful_life) - ) / flt(fb_row.total_number_of_depreciations) - # if the Depreciation Schedule is being prepared for the first time - else: - depreciation_amount = ( - flt(self.gross_purchase_amount) - flt(fb_row.expected_value_after_useful_life) - ) / flt(fb_row.total_number_of_depreciations) - else: - depreciation_amount = flt(depreciable_value * (flt(fb_row.rate_of_depreciation) / 100)) - - return depreciation_amount - def validate_make_gl_entry(self): purchase_document = self.get_purchase_document() if not purchase_document: @@ -618,7 +567,12 @@ class Asset(AccountsController): float_precision = cint(frappe.db.get_default("float_precision")) or 2 if args.get("depreciation_method") == "Double Declining Balance": - return 200.0 / args.get("total_number_of_depreciations") + return 200.0 / ( + ( + flt(args.get("total_number_of_depreciations"), 2) * flt(args.get("frequency_of_depreciation")) + ) + / 12 + ) if args.get("depreciation_method") == "Written Down Value": if ( @@ -635,17 +589,20 @@ class Asset(AccountsController): else: value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount) - depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2)) + depreciation_rate = math.pow( + value, + 1.0 + / ( + ( + flt(args.get("total_number_of_depreciations"), 2) + * flt(args.get("frequency_of_depreciation")) + ) + / 12 + ), + ) return flt((100 * (1 - depreciation_rate)), float_precision) - def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date): - days = date_diff(to_date, from_date) - months = month_diff(to_date, from_date) - total_days = get_total_days(to_date, row.frequency_of_depreciation) - - return (depreciation_amount * flt(days)) / flt(total_days), days, months - def update_maintenance_status(): assets = frappe.get_all( @@ -889,15 +846,6 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None): return asset.get_value_after_depreciation(finance_book) -def get_total_days(date, frequency): - period_start_date = add_months(date, cint(frequency) * -1) - - if is_last_day_of_the_month(date): - period_start_date = get_last_day(period_start_date) - - return date_diff(date, period_start_date) - - @frappe.whitelist() def split_asset(asset_name, split_qty): asset = frappe.get_doc("Asset", asset_name) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 2c9772d12a..cde02809f1 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -29,8 +29,11 @@ from erpnext.assets.doctype.asset.depreciation import ( scrap_asset, ) from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + _check_is_pro_rata, + _get_pro_rata_amt, get_asset_depr_schedule_doc, get_depr_schedule, + get_depreciation_amount, ) from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice as make_invoice, @@ -234,7 +237,7 @@ class TestAsset(AssetSetup): asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, asset.precision("gross_purchase_amount"), ) - pro_rata_amount, _, _ = asset.get_pro_rata_amt( + pro_rata_amount, _, _ = _get_pro_rata_amt( asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date ) pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) @@ -321,7 +324,7 @@ class TestAsset(AssetSetup): self.assertEquals(second_asset_depr_schedule.status, "Active") self.assertEquals(first_asset_depr_schedule.status, "Cancelled") - pro_rata_amount, _, _ = asset.get_pro_rata_amt( + pro_rata_amount, _, _ = _get_pro_rata_amt( asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date ) pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) @@ -857,12 +860,12 @@ class TestDepreciationMethods(AssetSetup): ) expected_schedules = [ - ["2022-02-28", 647.25, 647.25], - ["2022-03-31", 1210.71, 1857.96], - ["2022-04-30", 1053.99, 2911.95], - ["2022-05-31", 917.55, 3829.5], - ["2022-06-30", 798.77, 4628.27], - ["2022-07-15", 371.73, 5000.0], + ["2022-02-28", 310.89, 310.89], + ["2022-03-31", 654.45, 965.34], + ["2022-04-30", 654.45, 1619.79], + ["2022-05-31", 654.45, 2274.24], + ["2022-06-30", 654.45, 2928.69], + ["2022-07-15", 2071.31, 5000.0], ] schedules = [ @@ -938,7 +941,7 @@ class TestDepreciationBasics(AssetSetup): }, ) - depreciation_amount = asset.get_depreciation_amount(100000, asset.finance_books[0]) + depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0]) self.assertEqual(depreciation_amount, 30000) def test_make_depr_schedule(self): @@ -997,7 +1000,7 @@ class TestDepreciationBasics(AssetSetup): }, ) - has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0]) + has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0]) self.assertFalse(has_pro_rata) asset.finance_books = [] @@ -1012,7 +1015,7 @@ class TestDepreciationBasics(AssetSetup): }, ) - has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0]) + has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0]) self.assertTrue(has_pro_rata) def test_expected_value_after_useful_life_greater_than_purchase_amount(self): diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index d23edfa4a1..116593ad9e 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -8,12 +8,16 @@ from frappe.utils import ( add_days, add_months, cint, + date_diff, flt, get_last_day, getdate, is_last_day_of_the_month, + month_diff, ) +import erpnext + class AssetDepreciationSchedule(Document): def before_save(self): @@ -185,7 +189,7 @@ class AssetDepreciationSchedule(Document): ): asset_doc.validate_asset_finance_books(row) - value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(asset_doc, row) + value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row) row.value_after_depreciation = value_after_depreciation if update_asset_finance_book_row: @@ -195,21 +199,46 @@ class AssetDepreciationSchedule(Document): self.number_of_depreciations_booked ) - has_pro_rata = asset_doc.check_is_pro_rata(row) + has_pro_rata = _check_is_pro_rata(asset_doc, row) if has_pro_rata: number_of_pending_depreciations += 1 + has_wdv_or_dd_non_yearly_pro_rata = False + if ( + row.depreciation_method in ("Written Down Value", "Double Declining Balance") + and cint(row.frequency_of_depreciation) != 12 + ): + has_wdv_or_dd_non_yearly_pro_rata = _check_is_pro_rata( + asset_doc, row, wdv_or_dd_non_yearly=True + ) + skip_row = False should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date) + depreciation_amount = 0 + for n in range(start, number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue - depreciation_amount = asset_doc.get_depreciation_amount(value_after_depreciation, row) + if n > 0 and len(self.get("depreciation_schedule")) > n - 1: + prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount + else: + prev_depreciation_amount = 0 - if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: + depreciation_amount = get_depreciation_amount( + asset_doc, + value_after_depreciation, + row, + n, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + ) + + if not has_pro_rata or ( + n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2 + ): schedule_date = add_months( row.depreciation_start_date, n * cint(row.frequency_of_depreciation) ) @@ -227,8 +256,11 @@ class AssetDepreciationSchedule(Document): if self.depreciation_schedule: from_date = self.depreciation_schedule[-1].schedule_date - depreciation_amount, days, months = asset_doc.get_pro_rata_amt( - row, depreciation_amount, from_date, date_of_disposal + depreciation_amount, days, months = _get_pro_rata_amt( + row, + depreciation_amount, + from_date, + date_of_disposal, ) if depreciation_amount > 0: @@ -240,12 +272,20 @@ class AssetDepreciationSchedule(Document): break # For first row - if has_pro_rata and not self.opening_accumulated_depreciation and n == 0: + if ( + (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) + and not self.opening_accumulated_depreciation + and n == 0 + ): from_date = add_days( asset_doc.available_for_use_date, -1 ) # needed to calc depr amount for available_for_use_date too - depreciation_amount, days, months = asset_doc.get_pro_rata_amt( - row, depreciation_amount, from_date, row.depreciation_start_date + depreciation_amount, days, months = _get_pro_rata_amt( + row, + depreciation_amount, + from_date, + row.depreciation_start_date, + has_wdv_or_dd_non_yearly_pro_rata, ) # For first depr schedule date will be the start date @@ -264,8 +304,12 @@ class AssetDepreciationSchedule(Document): depreciation_amount_without_pro_rata = depreciation_amount - depreciation_amount, days, months = asset_doc.get_pro_rata_amt( - row, depreciation_amount, schedule_date, asset_doc.to_date + depreciation_amount, days, months = _get_pro_rata_amt( + row, + depreciation_amount, + schedule_date, + asset_doc.to_date, + has_wdv_or_dd_non_yearly_pro_rata, ) depreciation_amount = self.get_adjusted_depreciation_amount( @@ -373,15 +417,142 @@ class AssetDepreciationSchedule(Document): accumulated_depreciation, d.precision("accumulated_depreciation_amount") ) - def _get_value_after_depreciation_for_making_schedule(self, asset_doc, fb_row): - if asset_doc.docstatus == 1 and fb_row.value_after_depreciation: - value_after_depreciation = flt(fb_row.value_after_depreciation) - else: - value_after_depreciation = flt(self.gross_purchase_amount) - flt( - self.opening_accumulated_depreciation - ) - return value_after_depreciation +def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row): + if asset_doc.docstatus == 1 and fb_row.value_after_depreciation: + value_after_depreciation = flt(fb_row.value_after_depreciation) + else: + value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt( + asset_doc.opening_accumulated_depreciation + ) + + return value_after_depreciation + + +# if it returns True, depreciation_amount will not be equal for the first and last rows +def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False): + has_pro_rata = False + + # if not existing asset, from_date = available_for_use_date + # otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 + # from_date = 01/01/2022 + from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly) + days = date_diff(row.depreciation_start_date, from_date) + 1 + + if wdv_or_dd_non_yearly: + total_days = get_total_days(row.depreciation_start_date, 12) + else: + # if frequency_of_depreciation is 12 months, total_days = 365 + total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) + + if days < total_days: + has_pro_rata = True + + return has_pro_rata + + +def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False): + if wdv_or_dd_non_yearly: + return add_months( + asset_doc.available_for_use_date, + (asset_doc.number_of_depreciations_booked * 12), + ) + else: + return add_months( + asset_doc.available_for_use_date, + (asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation), + ) + + +def _get_pro_rata_amt( + row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False +): + days = date_diff(to_date, from_date) + months = month_diff(to_date, from_date) + if has_wdv_or_dd_non_yearly_pro_rata: + total_days = get_total_days(to_date, 12) + else: + total_days = get_total_days(to_date, row.frequency_of_depreciation) + + return (depreciation_amount * flt(days)) / flt(total_days), days, months + + +def get_total_days(date, frequency): + period_start_date = add_months(date, cint(frequency) * -1) + + if is_last_day_of_the_month(date): + period_start_date = get_last_day(period_start_date) + + return date_diff(date, period_start_date) + + +@erpnext.allow_regional +def get_depreciation_amount( + asset, + depreciable_value, + row, + schedule_idx=0, + prev_depreciation_amount=0, + has_wdv_or_dd_non_yearly_pro_rata=False, +): + if row.depreciation_method in ("Straight Line", "Manual"): + return get_straight_line_or_manual_depr_amount(asset, row) + else: + return get_wdv_or_dd_depr_amount( + depreciable_value, + row.rate_of_depreciation, + row.frequency_of_depreciation, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + ) + + +def get_straight_line_or_manual_depr_amount(asset, row): + # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value + if asset.flags.increase_in_asset_life: + return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / ( + date_diff(asset.to_date, asset.available_for_use_date) / 365 + ) + # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value + elif asset.flags.increase_in_asset_value_due_to_repair: + return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt( + row.total_number_of_depreciations + ) + # if the Depreciation Schedule is being prepared for the first time + else: + return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt( + row.total_number_of_depreciations + ) + + +def get_wdv_or_dd_depr_amount( + depreciable_value, + rate_of_depreciation, + frequency_of_depreciation, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, +): + if cint(frequency_of_depreciation) == 12: + return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) + else: + if has_wdv_or_dd_non_yearly_pro_rata: + if schedule_idx == 0: + return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) + elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1: + return ( + flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) + ) + else: + return prev_depreciation_amount + else: + if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0: + return ( + flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) + ) + else: + return prev_depreciation_amount def make_draft_asset_depr_schedules_if_not_present(asset_doc): From e6a9b6ee9555ac4814a2526199bd646f6701502f Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 5 Apr 2023 08:26:15 +0200 Subject: [PATCH 64/77] feat: remove deprecated get_customer_list (#34624) feat: remove deprecated method get_customer_list --- erpnext/selling/doctype/customer/customer.py | 45 -------------------- 1 file changed, 45 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index ebafddb061..18336d2c9f 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -11,7 +11,6 @@ from frappe.contacts.address_and_contact import ( delete_contact_and_address, load_address_and_contact, ) -from frappe.desk.reportview import build_match_conditions, get_filters_cond from frappe.model.mapper import get_mapped_doc from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options from frappe.model.utils.rename_doc import update_linked_doctypes @@ -445,50 +444,6 @@ def get_nested_links(link_doctype, link_name, ignore_permissions=False): return links -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): - from frappe.utils.deprecations import deprecation_warning - - from erpnext.controllers.queries import get_fields - - deprecation_warning( - "`get_customer_list` is deprecated and will be removed in version 15. Use `erpnext.controllers.queries.customer_query` instead." - ) - - fields = ["name", "customer_name", "customer_group", "territory"] - - if frappe.db.get_default("cust_master_name") == "Customer Name": - fields = ["name", "customer_group", "territory"] - - fields = get_fields("Customer", fields) - - match_conditions = build_match_conditions("Customer") - match_conditions = "and {}".format(match_conditions) if match_conditions else "" - - if filters: - filter_conditions = get_filters_cond(doctype, filters, []) - match_conditions += "{}".format(filter_conditions) - - return frappe.db.sql( - """ - select %s - from `tabCustomer` - where docstatus < 2 - and (%s like %s or customer_name like %s) - {match_conditions} - order by - case when name like %s then 0 else 1 end, - case when customer_name like %s then 0 else 1 end, - name, customer_name limit %s, %s - """.format( - match_conditions=match_conditions - ) - % (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), - ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len), - ) - - def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, extra_amount=0): credit_limit = get_credit_limit(customer, company) if not credit_limit: From fd3fb64aa344b8a220f217ac6d2ffd09dbcdf0dc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 5 Apr 2023 12:02:44 +0530 Subject: [PATCH 65/77] feat: Auto allocate advance payments only against orders (#34727) feat: Auto allocate advance payments only againt orders --- .../doctype/purchase_invoice/purchase_invoice.json | 12 ++++++++++-- .../doctype/sales_invoice/sales_invoice.json | 11 ++++++++++- erpnext/controllers/accounts_controller.py | 4 +++- erpnext/public/js/controllers/accounts.js | 8 ++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 54caf6f8b0..b4d369e6c6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -118,6 +118,7 @@ "paid_amount", "advances_section", "allocate_advances_automatically", + "only_include_allocated_payments", "get_advances", "advances", "advance_tax", @@ -1550,17 +1551,24 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "default": "0", + "depends_on": "allocate_advances_automatically", + "description": "Advance payments allocated against orders will only be fetched", + "fieldname": "only_include_allocated_payments", + "fieldtype": "Check", + "label": "Only Include Allocated Payments" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-01-28 19:18:56.586321", + "modified": "2023-04-03 22:57:14.074982", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 2a8ff40413..a41e13c8ea 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -120,6 +120,7 @@ "account_for_change_amount", "advances_section", "allocate_advances_automatically", + "only_include_allocated_payments", "get_advances", "advances", "write_off_section", @@ -2126,6 +2127,14 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "default": "0", + "depends_on": "allocate_advances_automatically", + "description": "Advance payments allocated against orders will only be fetched", + "fieldname": "only_include_allocated_payments", + "fieldtype": "Check", + "label": "Only Include Allocated Payments" } ], "icon": "fa fa-file-text", @@ -2138,7 +2147,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-03-13 11:43:15.883055", + "modified": "2023-04-03 22:55:14.206473", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 390af0deb2..a347323e35 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -845,7 +845,9 @@ class AccountsController(TransactionBase): def set_advances(self): """Returns list of advances against Account, Party, Reference""" - res = self.get_advance_entries() + res = self.get_advance_entries( + include_unallocated=not cint(self.get("only_include_allocated_payments")) + ) self.set("advances", []) advance_allocated = 0 diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index a07f75d1c5..d943126018 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -55,6 +55,14 @@ frappe.ui.form.on(cur_frm.doctype, { }, allocate_advances_automatically: function(frm) { + frm.trigger('fetch_advances'); + }, + + only_include_allocated_payments: function(frm) { + frm.trigger('fetch_advances'); + }, + + fetch_advances: function(frm) { if(frm.doc.allocate_advances_automatically) { frappe.call({ doc: frm.doc, From f193393f5713fd274ac419ecc786057415266d38 Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Wed, 5 Apr 2023 12:04:36 +0530 Subject: [PATCH 66/77] fix!: require sender and message for contact us page (#34707) * fix: require sender and message for contact us page * refactor: dont override frappe.send_message from client side used override_whitelisted_method hook for the same --- erpnext/hooks.py | 4 ++++ erpnext/public/js/website_utils.js | 15 --------------- erpnext/templates/utils.py | 9 +++------ 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 0cb35c1de8..862a546a76 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -28,6 +28,10 @@ doctype_js = { override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"} +override_whitelisted_methods = { + "frappe.www.contact.send_message": "erpnext.templates.utils.send_message" +} + welcome_email = "erpnext.setup.utils.welcome_email" # setup wizard diff --git a/erpnext/public/js/website_utils.js b/erpnext/public/js/website_utils.js index b5416065d7..2bb5255eeb 100644 --- a/erpnext/public/js/website_utils.js +++ b/erpnext/public/js/website_utils.js @@ -3,18 +3,6 @@ if(!window.erpnext) window.erpnext = {}; -// Add / update a new Lead / Communication -// subject, sender, description -frappe.send_message = function(opts, btn) { - return frappe.call({ - type: "POST", - method: "erpnext.templates.utils.send_message", - btn: btn, - args: opts, - callback: opts.callback - }); -}; - erpnext.subscribe_to_newsletter = function(opts, btn) { return frappe.call({ type: "POST", @@ -24,6 +12,3 @@ erpnext.subscribe_to_newsletter = function(opts, btn) { callback: opts.callback }); } - -// for backward compatibility -erpnext.send_message = frappe.send_message; diff --git a/erpnext/templates/utils.py b/erpnext/templates/utils.py index 48b44802a8..57750a56f6 100644 --- a/erpnext/templates/utils.py +++ b/erpnext/templates/utils.py @@ -6,13 +6,12 @@ import frappe @frappe.whitelist(allow_guest=True) -def send_message(subject="Website Query", message="", sender="", status="Open"): +def send_message(sender, message, subject="Website Query"): from frappe.www.contact import send_message as website_send_message + website_send_message(sender, message, subject) + lead = customer = None - - website_send_message(subject, message, sender) - customer = frappe.db.sql( """select distinct dl.link_name from `tabDynamic Link` dl left join `tabContact` c on dl.parent=c.name where dl.link_doctype='Customer' @@ -58,5 +57,3 @@ def send_message(subject="Website Query", message="", sender="", status="Open"): } ) comm.insert(ignore_permissions=True) - - return "okay" From 56f50783576c23dff9fcf7aab5c8627abc23eaf8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 5 Apr 2023 12:43:32 +0530 Subject: [PATCH 67/77] fix: Shop by category fixes (#34688) * fix: Shop by category fixes * chore: Update tests --- .../doctype/website_item/test_website_item.py | 8 +++++++- erpnext/setup/doctype/item_group/item_group.py | 9 +++++++-- erpnext/www/shop-by-category/index.py | 12 +++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py index e41c9da594..43b2f67571 100644 --- a/erpnext/e_commerce/doctype/website_item/test_website_item.py +++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py @@ -199,8 +199,14 @@ class TestWebsiteItem(unittest.TestCase): breadcrumbs = get_parent_item_groups(item.item_group) + settings = frappe.get_cached_doc("E Commerce Settings") + if settings.enable_field_filters: + base_breadcrumb = "Shop by Category" + else: + base_breadcrumb = "All Products" + self.assertEqual(breadcrumbs[0]["name"], "Home") - self.assertEqual(breadcrumbs[1]["name"], "All Products") + self.assertEqual(breadcrumbs[1]["name"], base_breadcrumb) self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1") diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 2fdfcf647d..2eca5cad8e 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -148,12 +148,17 @@ def get_item_for_list_in_html(context): def get_parent_item_groups(item_group_name, from_item=False): - base_nav_page = {"name": _("All Products"), "route": "/all-products"} + settings = frappe.get_cached_doc("E Commerce Settings") + + if settings.enable_field_filters: + base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"} + else: + base_nav_page = {"name": _("All Products"), "route": "/all-products"} if from_item and frappe.request.environ.get("HTTP_REFERER"): # base page after 'Home' will vary on Item page last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0] - if last_page and last_page == "shop-by-category": + if last_page and last_page in ("shop-by-category", "all-products"): base_nav_page_title = " ".join(last_page.split("-")).title() base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page} diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py index 219747c9f8..913c1836ac 100644 --- a/erpnext/www/shop-by-category/index.py +++ b/erpnext/www/shop-by-category/index.py @@ -53,6 +53,7 @@ def get_tabs(categories): def get_category_records(categories: list): categorical_data = {} + website_item_meta = frappe.get_meta("Website Item", cached=True) for c in categories: if c == "item_group": @@ -64,7 +65,16 @@ def get_category_records(categories: list): continue - doctype = frappe.unscrub(c) + field_type = website_item_meta.get_field(c).fieldtype + + if field_type == "Table MultiSelect": + child_doc = website_item_meta.get_field(c).options + for field in frappe.get_meta(child_doc, cached=True).fields: + if field.fieldtype == "Link" and field.reqd: + doctype = field.options + else: + doctype = website_item_meta.get_field(c).options + fields = ["name"] try: From 9bf87d708e2fe585a66a565deb6a334c18fb301f Mon Sep 17 00:00:00 2001 From: Lucky-Tsuma <55623011+Lucky-Tsuma@users.noreply.github.com> Date: Wed, 5 Apr 2023 01:22:00 -0700 Subject: [PATCH 68/77] fix: `payment entry is already created` on posawesome. (#34712) --- .../doctype/payment_request/payment_request.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 7005c17362..5f5647c7ec 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -497,10 +497,16 @@ def get_amount(ref_doc, payment_account=None): if dt in ["Sales Order", "Purchase Order"]: grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) elif dt in ["Sales Invoice", "Purchase Invoice"]: - if ref_doc.party_account_currency == ref_doc.currency: - grand_total = flt(ref_doc.outstanding_amount) - else: - grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate + if not ref_doc.is_pos: + if ref_doc.party_account_currency == ref_doc.currency: + grand_total = flt(ref_doc.outstanding_amount) + else: + grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate + elif dt == "Sales Invoice": + for pay in ref_doc.payments: + if pay.type == "Phone" and pay.account == payment_account: + grand_total = pay.amount + break elif dt == "POS Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: From bc39dfab5d91c67d4ea6fe628eae7914937c38fa Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 6 Apr 2023 09:29:44 +0530 Subject: [PATCH 69/77] feat: add `Received Qty` field in `Delivery Note Item` --- .../delivery_note_item/delivery_note_item.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 1763269193..180adee0cb 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -83,6 +83,8 @@ "actual_qty", "installed_qty", "item_tax_rate", + "column_break_atna", + "received_qty", "accounting_details_section", "expense_account", "allow_zero_valuation_rate", @@ -832,13 +834,27 @@ "fieldname": "material_request_item", "fieldtype": "Data", "label": "Material Request Item" + }, + { + "fieldname": "column_break_atna", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: parent.is_internal_customer", + "fieldname": "received_qty", + "fieldtype": "Float", + "label": "Received Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "report_hide": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-03-20 14:24:10.406746", + "modified": "2023-04-06 09:28:29.182053", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", From 0d1df26b88ad81018bb1e2f3c8415e2e002c9172 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 6 Apr 2023 09:31:46 +0530 Subject: [PATCH 70/77] chore: add `Delivery Note Item` in Purchase Receipt `Status Updater` --- .../stock/doctype/purchase_receipt/purchase_receipt.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c1abd31bcc..d268cc1196 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -65,6 +65,16 @@ class PurchaseReceipt(BuyingController): "percent_join_field": "purchase_invoice", "overflow_type": "receipt", }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Delivery Note Item", + "join_field": "delivery_note_item", + "source_field": "received_qty", + "target_field": "received_qty", + "target_parent_dt": "Delivery Note", + "target_ref_field": "qty", + "overflow_type": "receipt", + }, ] if cint(self.is_return): From 91a26608ee6a8cb09547e2d5059a36ae4daaa0d9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 6 Apr 2023 12:40:58 +0530 Subject: [PATCH 71/77] fix: Unable to create payment request against purchase invoice (#34762) --- .../payment_request/payment_request.py | 2 +- .../payment_request/test_payment_request.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 5f5647c7ec..11d6d5f433 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -497,7 +497,7 @@ def get_amount(ref_doc, payment_account=None): if dt in ["Sales Order", "Purchase Order"]: grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) elif dt in ["Sales Invoice", "Purchase Invoice"]: - if not ref_doc.is_pos: + if not ref_doc.get("is_pos"): if ref_doc.party_account_currency == ref_doc.currency: grand_total = flt(ref_doc.outstanding_amount) else: diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 4279aa4f85..e17a846dd8 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -6,6 +6,7 @@ import unittest import frappe from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.setup.utils import get_exchange_rate @@ -74,6 +75,29 @@ class TestPaymentRequest(unittest.TestCase): self.assertEqual(pr.reference_name, si_usd.name) self.assertEqual(pr.currency, "USD") + def test_payment_entry_against_purchase_invoice(self): + si_usd = make_purchase_invoice( + customer="_Test Supplier USD", + debit_to="_Test Payable USD - _TC", + currency="USD", + conversion_rate=50, + ) + + pr = make_payment_request( + dt="Purchase Invoice", + dn=si_usd.name, + recipient_id="user@example.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + submit_doc=1, + return_doc=1, + ) + + pe = pr.create_payment_entry() + pr.load_from_db() + + self.assertEqual(pr.status, "Paid") + def test_payment_entry(self): frappe.db.set_value( "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" From 82a136f991d801051c7a071017e6bb9ed58eac06 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 6 Apr 2023 12:51:32 +0530 Subject: [PATCH 72/77] fix: UX for stock entry, bom and work order --- erpnext/manufacturing/doctype/bom/bom.json | 15 +++++------ .../doctype/work_order/work_order.json | 25 +++++++++++-------- .../doctype/stock_entry/stock_entry.json | 15 +++++------ 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index db699b94d8..d02402299e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -9,15 +9,14 @@ "production_item_tab", "item", "company", - "item_name", "uom", + "quantity", "cb0", "is_active", "is_default", "allow_alternative_item", "set_rate_of_sub_assembly_item_based_on_bom", "project", - "quantity", "image", "currency_detail", "rm_cost_as_per", @@ -27,6 +26,8 @@ "column_break_ivyw", "currency", "conversion_rate", + "materials_section", + "items", "section_break_21", "operations_section_section", "with_operations", @@ -38,8 +39,6 @@ "operating_cost_per_bom_quantity", "operations_section", "operations", - "materials_section", - "items", "scrap_section", "scrap_items_section", "scrap_items", @@ -59,6 +58,7 @@ "total_cost", "base_total_cost", "more_info_tab", + "item_name", "description", "column_break_27", "has_variants", @@ -192,6 +192,7 @@ "options": "Quality Inspection Template" }, { + "collapsible": 1, "fieldname": "currency_detail", "fieldtype": "Section Break", "label": "Cost Configuration" @@ -417,7 +418,7 @@ { "collapsible": 1, "fieldname": "website_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Website" }, { @@ -482,7 +483,7 @@ { "fieldname": "section_break_21", "fieldtype": "Tab Break", - "label": "Operations & Materials" + "label": "Operations" }, { "fieldname": "column_break_23", @@ -605,7 +606,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-02-13 17:31:37.504565", + "modified": "2023-04-06 12:47:58.514795", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 25e16d6337..aa9049801c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -22,17 +22,13 @@ "produced_qty", "process_loss_qty", "project", - "serial_no_and_batch_for_finished_good_section", - "has_serial_no", - "has_batch_no", - "column_break_17", - "serial_no", - "batch_size", + "section_break_ndpq", + "required_items", "work_order_configuration", "settings_section", "allow_alternative_item", "use_multi_level_bom", - "column_break_18", + "column_break_17", "skip_transfer", "from_wip_warehouse", "update_consumed_material_cost_in_project", @@ -42,9 +38,14 @@ "column_break_12", "fg_warehouse", "scrap_warehouse", + "serial_no_and_batch_for_finished_good_section", + "has_serial_no", + "has_batch_no", + "column_break_18", + "serial_no", + "batch_size", "required_items_section", "materials_and_operations_tab", - "required_items", "operations_section", "operations", "transfer_material_against", @@ -586,7 +587,11 @@ { "fieldname": "materials_and_operations_tab", "fieldtype": "Tab Break", - "label": "Materials & Operations" + "label": "Operations" + }, + { + "fieldname": "section_break_ndpq", + "fieldtype": "Section Break" } ], "icon": "fa fa-cogs", @@ -594,7 +599,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-01-03 14:16:35.427731", + "modified": "2023-04-06 12:35:12.149827", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 9c0f1fc03f..bc5533fd2d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -27,7 +27,6 @@ "set_posting_time", "inspection_required", "apply_putaway_rule", - "items_tab", "bom_info_section", "from_bom", "use_multi_level_bom", @@ -256,7 +255,7 @@ "description": "As per Stock UOM", "fieldname": "fg_completed_qty", "fieldtype": "Float", - "label": "For Quantity", + "label": "Finished Good Quantity ", "oldfieldname": "fg_completed_qty", "oldfieldtype": "Currency", "print_hide": 1 @@ -612,11 +611,7 @@ "read_only": 1 }, { - "fieldname": "items_tab", - "fieldtype": "Tab Break", - "label": "Items" - }, - { + "collapsible": 1, "fieldname": "bom_info_section", "fieldtype": "Section Break", "label": "BOM Info" @@ -644,8 +639,10 @@ "oldfieldtype": "Section Break" }, { + "collapsible": 1, "fieldname": "section_break_7qsm", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Process Loss" }, { "depends_on": "process_loss_percentage", @@ -677,7 +674,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-01-03 16:02:50.741816", + "modified": "2023-04-06 12:42:56.673180", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", From a55b8181192c89df2a19dc815950e3e7dab1bd34 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 5 Apr 2023 13:20:32 +0530 Subject: [PATCH 73/77] fix: Subcontracting Receipt incorrect `status` --- .../subcontracting_receipt.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 95dbc83bf8..4f8e045d70 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -245,17 +245,17 @@ class SubcontractingReceipt(SubcontractingController): item.expense_account = expense_account def update_status(self, status=None, update_modified=False): - if self.docstatus >= 1 and not status: - if self.docstatus == 1: + if not status: + if self.docstatus == 0: + status = "Draft" + elif self.docstatus == 1: + status = "Completed" if self.is_return: status = "Return" return_against = frappe.get_doc("Subcontracting Receipt", self.return_against) return_against.run_method("update_status") - else: - if self.per_returned == 100: - status = "Return Issued" - elif self.status == "Draft": - status = "Completed" + elif self.per_returned == 100: + status = "Return Issued" elif self.docstatus == 2: status = "Cancelled" From a575bd50efbc93d151b76cf6d0d08ce87101d957 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 6 Apr 2023 14:59:14 +0530 Subject: [PATCH 74/77] test: add test cases for internal PR received qty --- .../purchase_receipt/test_purchase_receipt.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b6341466f8..7567cfe98c 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1544,6 +1544,72 @@ class TestPurchaseReceipt(FrappeTestCase): res = get_item_details(args) self.assertEqual(res.get("last_purchase_rate"), 100) + def test_validate_received_qty_for_internal_pr(self): + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) + + # Step 1: Create Item + item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}) + + # Step 2: Create Stock Entry (Material Receipt) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + make_stock_entry( + purpose="Material Receipt", + item_code=item.name, + qty=15, + company=company, + to_warehouse=from_warehouse, + ) + + # Step 3: Create Delivery Note with Internal Customer + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + dn = create_delivery_note( + item_code=item.name, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=10, + rate=100, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + ) + + # Step 4: Create Internal Purchase Receipt + from erpnext.controllers.status_updater import OverAllowanceError + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + + pr = make_inter_company_purchase_receipt(dn.name) + pr.items[0].qty = 15 + pr.items[0].from_warehouse = target_warehouse + pr.items[0].warehouse = to_warehouse + pr.items[0].rejected_warehouse = from_warehouse + pr.save() + + self.assertRaises(OverAllowanceError, pr.submit) + + # Step 5: Test Over Receipt Allowance + frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50) + + make_stock_entry( + purpose="Material Transfer", + item_code=item.name, + qty=5, + company=company, + from_warehouse=from_warehouse, + to_warehouse=target_warehouse, + ) + + pr.submit() + + frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 984905109d18c88a9d0c0c3292002c47a7edfadb Mon Sep 17 00:00:00 2001 From: anandbaburajan Date: Fri, 7 Apr 2023 15:23:02 +0530 Subject: [PATCH 75/77] fix: improper usage of get_depreciation_amount in asset_value_adjustment --- .../doctype/asset_value_adjustment/asset_value_adjustment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 31d6ffab5f..021332883d 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -14,6 +14,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciatio from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( get_asset_depr_schedule_doc, + get_depreciation_amount, ) @@ -162,7 +163,7 @@ class AssetValueAdjustment(Document): depreciation_amount = days * rate_per_day from_date = data.schedule_date else: - depreciation_amount = asset.get_depreciation_amount(value_after_depreciation, d) + depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d) if depreciation_amount: value_after_depreciation -= flt(depreciation_amount) From 6f6928fa7bb20959b34d82e283dd80b1956c9a26 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 9 Apr 2023 09:38:13 +0530 Subject: [PATCH 76/77] fix: Item tax validity comparison fixes (#34784) fix: Item tax validity comparsion fixes --- erpnext/setup/doctype/item_group/item_group.py | 16 ++++++++++++++++ erpnext/stock/doctype/item/item.py | 11 ++++++++--- erpnext/stock/get_item_details.py | 4 +++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 2eca5cad8e..f5432c1825 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -36,8 +36,24 @@ class ItemGroup(NestedSet, WebsiteGenerator): self.make_route() self.validate_item_group_defaults() + self.check_item_tax() ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True) + def check_item_tax(self): + """Check whether Tax Rate is not entered twice for same Tax Type""" + check_list = [] + for d in self.get("taxes"): + if d.item_tax_template: + if (d.item_tax_template, d.tax_category) in check_list: + frappe.throw( + _("{0} entered twice {1} in Item Taxes").format( + frappe.bold(d.item_tax_template), + "for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "", + ) + ) + else: + check_list.append((d.item_tax_template, d.tax_category)) + def on_update(self): NestedSet.on_update(self) invalidate_cache_for(self) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 3f0ac3e2ba..3cc59bed19 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -351,10 +351,15 @@ class Item(Document): check_list = [] for d in self.get("taxes"): if d.item_tax_template: - if d.item_tax_template in check_list: - frappe.throw(_("{0} entered twice in Item Tax").format(d.item_tax_template)) + if (d.item_tax_template, d.tax_category) in check_list: + frappe.throw( + _("{0} entered twice {1} in Item Taxes").format( + frappe.bold(d.item_tax_template), + "for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "", + ) + ) else: - check_list.append(d.item_tax_template) + check_list.append((d.item_tax_template, d.tax_category)) def validate_barcode(self): import barcodenumber diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 2df39c8183..2cf3797a36 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -637,7 +637,9 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity.append(tax) if taxes_with_validity: - taxes = sorted(taxes_with_validity, key=lambda i: i.valid_from, reverse=True) + taxes = sorted( + taxes_with_validity, key=lambda i: i.valid_from or tax.maximum_net_rate, reverse=True + ) else: taxes = taxes_with_no_validity From 934e1b4e6a7a95544cb3226535cf31a22d6552bd Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sun, 9 Apr 2023 15:34:57 +0200 Subject: [PATCH 77/77] fix: add german translation of "Partly Paid" (#34776) --- erpnext/translations/de.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 9599eed634..57556f3ef1 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1875,6 +1875,7 @@ Parents Teacher Meeting Attendance,Eltern Lehrer Treffen Teilnahme, Part-time,Teilzeit, Partially Depreciated,Teilweise abgeschrieben, Partially Received,Teilweise erhalten, +Partly Paid,Teilweise bezahlt, Party,Partei, Party Name,Name der Partei, Party Type,Partei-Typ,