From 9d256e131df0a3e73993ef6b8faffc7b883ca89f Mon Sep 17 00:00:00 2001 From: RJPvT <48353029+RJPvT@users.noreply.github.com> Date: Tue, 2 Jan 2024 22:01:51 +0100 Subject: [PATCH 01/48] fix: wrong file name file name is actually reversed. In [1]: from premailer import Premailer ...: ...: from frappe.utils.jinja_globals import bundled_asset ...: ...: # get email css files from hooks ...: css_files = frappe.get_hooks("email_css") ...: css_files = [frappe.utils.jinja_globals.bundled_asset(path) for path in ...: css_files] ...: css_files = [path.lstrip("/") for path in css_files] In [2]: css_files Out[2]: ['assets/frappe/dist/css/email.bundle.ELTO33N3.css', 'email_erpnext.bundle.css', 'assets/css/email.css'] In [3]: css_files = [css_file for css_file in css_files if os.path.exists(os.pat ...: h.abspath(css_file))] In [4]: css_files Out[4]: ['assets/frappe/dist/css/email.bundle.ELTO33N3.css', 'assets/css/email.css'] --- erpnext/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 054f9dedabe..884e6bfcabe 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -18,7 +18,7 @@ app_include_js = "erpnext.bundle.js" app_include_css = "erpnext.bundle.css" web_include_js = "erpnext-web.bundle.js" web_include_css = "erpnext-web.bundle.css" -email_css = "email_erpnext.bundle.css" +email_css = "erpnext_email.bundle.scss" doctype_js = { "Address": "public/js/address.js", From 9277b0255748f425cf7eec51ed1ef0f351924bf9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 2 Jan 2024 15:18:15 +0530 Subject: [PATCH 02/48] refactor: flag to control loyalty point creation at invoice level (cherry picked from commit 1bc74bde291f92f450497fef6d30c86a0c0209d2) --- .../doctype/sales_invoice/sales_invoice.json | 14 +++++++++++--- .../doctype/sales_invoice/sales_invoice.py | 7 ++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 0c337aa62ec..83b7da94110 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -138,6 +138,7 @@ "loyalty_amount", "column_break_77", "loyalty_program", + "dont_create_loyalty_points", "loyalty_redemption_account", "loyalty_redemption_cost_center", "contact_and_address_tab", @@ -1039,8 +1040,7 @@ "label": "Loyalty Program", "no_copy": 1, "options": "Loyalty Program", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -2153,6 +2153,14 @@ "fieldname": "update_billed_amount_in_delivery_note", "fieldtype": "Check", "label": "Update Billed Amount in Delivery Note" + }, + { + "default": "0", + "depends_on": "loyalty_program", + "fieldname": "dont_create_loyalty_points", + "fieldtype": "Check", + "label": "Don't Create Loyalty Points", + "no_copy": 1 } ], "icon": "fa fa-file-text", @@ -2165,7 +2173,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-11-23 16:56:29.679499", + "modified": "2024-01-02 17:25:46.027523", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 63a576b5ba8..66d9022d08f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -300,7 +300,12 @@ class SalesInvoice(SellingController): update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) # create the loyalty point ledger entry if the customer is enrolled in any loyalty program - if not self.is_return and not self.is_consolidated and self.loyalty_program: + if ( + not self.is_return + and not self.is_consolidated + and self.loyalty_program + and not self.dont_create_loyalty_points + ): self.make_loyalty_point_entry() elif ( self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program From 36b8e972f11cb860c06ca3ca1726103be3217bb4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 3 Jan 2024 20:56:12 +0530 Subject: [PATCH 03/48] fix: ignore cancelled payments in Sales/Purchase Register (cherry picked from commit 0f1be03faf96aaf83063e28dc57a5e016944683c) --- erpnext/accounts/report/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 7dd61d2c64c..01bc29fedf3 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -251,6 +251,7 @@ def get_journal_entries(filters, args): ) .where( (je.voucher_type == "Journal Entry") + & (je.docstatus == 1) & (journal_account.party == filters.get(args.party)) & (journal_account.account == args.party_account) ) @@ -281,7 +282,9 @@ def get_payment_entries(filters, args): pe.cost_center, ) .where( - (pe.party == filters.get(args.party)) & (pe[args.account_fieldname] == args.party_account) + (pe.docstatus == 1) + & (pe.party == filters.get(args.party)) + & (pe[args.account_fieldname].isin(args.party_account)) ) .orderby(pe.posting_date, pe.name, order=Order.desc) ) From d42db1174d3f5ffd2f0517e2a39ae10ad05e4eec Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 10 Jan 2024 21:31:03 +0530 Subject: [PATCH 04/48] feat: provision to select the qty field for Product Page (#39292) feat: provision to select the qty field to be shown as `In Stock` in product page --- .../e_commerce_settings.json | 10 ++++++- erpnext/patches.txt | 1 + erpnext/utilities/product.py | 29 +++++++++++++------ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index e6f08f708a8..31b3197e12d 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -31,6 +31,7 @@ "column_break_21", "default_customer_group", "quotation_series", + "show_actual_qty", "checkout_settings_section", "enable_checkout", "show_price_in_quotation", @@ -366,12 +367,19 @@ "fieldtype": "Check", "label": "Enable Redisearch", "read_only_depends_on": "eval:!doc.is_redisearch_loaded" + }, + { + "default": "1", + "description": "If enabled Actual Qty will be shown as In Stock on the product page instead of Projected Qty.", + "fieldname": "show_actual_qty", + "fieldtype": "Check", + "label": "Show Actual Qty" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-04-01 18:35:56.106756", + "modified": "2024-01-10 21:06:45.386977", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e464552fa0c..6b7b13ff46e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -357,3 +357,4 @@ erpnext.patches.v14_0.update_total_asset_cost_field erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 erpnext.patches.v14_0.set_maintain_stock_for_bom_item +execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1) diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index 5b8b6c67c79..49066cd0c8a 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.query_builder.functions import IfNull from frappe.utils import cint, flt, fmt_money, getdate, nowdate from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item @@ -30,16 +31,26 @@ def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None): total_stock = 0.0 if warehouses: + qty_field = ( + "actual_qty" + if frappe.db.get_single_value("E Commerce Settings", "show_actual_qty") + else "projected_qty" + ) + + BIN = frappe.qb.DocType("Bin") + ITEM = frappe.qb.DocType("Item") + UOM = frappe.qb.DocType("UOM Conversion Detail") + for warehouse in warehouses: - stock_qty = frappe.db.sql( - """ - select S.actual_qty / IFNULL(C.conversion_factor, 1) - from tabBin S - inner join `tabItem` I on S.item_code = I.Item_code - left join `tabUOM Conversion Detail` C on I.sales_uom = C.uom and C.parent = I.Item_code - where S.item_code=%s and S.warehouse=%s""", - (item_code, warehouse), - ) + stock_qty = ( + frappe.qb.from_(BIN) + .select(BIN[qty_field] / IfNull(UOM.conversion_factor, 1)) + .inner_join(ITEM) + .on(BIN.item_code == ITEM.item_code) + .left_join(UOM) + .on((ITEM.sales_uom == UOM.uom) & (UOM.parent == ITEM.item_code)) + .where((BIN.item_code == item_code) & (BIN.warehouse == warehouse)) + ).run() if stock_qty: total_stock += adjust_qty_for_expired_items(item_code, stock_qty, warehouse) From dc7c9e7affa89dab7daf94f68a7205e07b38763f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 10 Jan 2024 22:22:16 +0530 Subject: [PATCH 05/48] fix: performance issue related to stock entry (backport #39301) (#39302) fix: performance issue related to stock entry (#39301) (cherry picked from commit c67b0a3a6408075785211da20603fbcd829825bb) Co-authored-by: rohitwaghchaure --- .../manufacturing/doctype/production_plan/production_plan.js | 2 +- .../manufacturing/doctype/production_plan/production_plan.py | 4 ++-- .../doctype/production_plan/test_production_plan.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index cd92263543b..c9c474db7f0 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -173,7 +173,7 @@ frappe.ui.form.on('Production Plan', { method: "set_status", freeze: true, doc: frm.doc, - args: {close : close}, + args: {close : close, update_bin: true}, callback: function() { frm.reload_doc(); } diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 767b4ccbb1a..c98d639663a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -503,7 +503,7 @@ class ProductionPlan(Document): frappe.delete_doc("Work Order", d.name) @frappe.whitelist() - def set_status(self, close=None): + def set_status(self, close=None, update_bin=False): self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus) if close: @@ -523,7 +523,7 @@ class ProductionPlan(Document): if close is not None: self.db_set("status", self.status) - if self.docstatus == 1 and self.status != "Completed": + if update_bin and self.docstatus == 1 and self.status != "Completed": self.update_bin_qty() def update_ordered_status(self): diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 4f6280ade8f..2b9751926a2 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1475,14 +1475,14 @@ class TestProductionPlan(FrappeTestCase): before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) pln.reload() - pln.set_status(close=True) + pln.set_status(close=True, update_bin=True) bin_name = get_or_make_bin(rm_item, rm_warehouse) after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) self.assertAlmostEqual(after_qty, before_qty - 10) pln.reload() - pln.set_status(close=False) + pln.set_status(close=False, update_bin=True) bin_name = get_or_make_bin(rm_item, rm_warehouse) after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) From dfcb7467741184da20d0f32d62433732a1acbec4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 10 Jan 2024 20:26:51 +0530 Subject: [PATCH 06/48] fix: circular dependency error while deleting QC (cherry picked from commit 7cc324e31ebf533b0ffde8a2a3c4013756959d7a) # Conflicts: # erpnext/stock/doctype/quality_inspection/test_quality_inspection.py --- .../quality_inspection/quality_inspection.py | 3 + .../test_quality_inspection.py | 64 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 2a9f091bd09..002f7bf8a8c 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -68,6 +68,9 @@ class QualityInspection(Document): def on_cancel(self): self.update_qc_reference() + def on_trash(self): + self.update_qc_reference() + def validate_readings_status_mandatory(self): for reading in self.readings: if not reading.status: diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 4f19643ad52..5c734744bae 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -216,6 +216,70 @@ class TestQualityInspection(FrappeTestCase): qa.save() self.assertEqual(qa.status, "Accepted") +<<<<<<< HEAD +======= + @change_settings("System Settings", {"number_format": "#.###,##"}) + def test_diff_number_format(self): + self.assertEqual(frappe.db.get_default("number_format"), "#.###,##") # sanity check + + # Test QI based on acceptance values (Non formula) + dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) + readings = [ + { + "specification": "Iron Content", # numeric reading + "min_value": 60, + "max_value": 100, + "reading_1": "70,000", + }, + { + "specification": "Iron Content", # numeric reading + "min_value": 60, + "max_value": 100, + "reading_1": "1.100,00", + }, + ] + + qa = create_quality_inspection( + reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True + ) + + qa.save() + + # status must be auto set as per formula + self.assertEqual(qa.readings[0].status, "Accepted") + self.assertEqual(qa.readings[1].status, "Rejected") + + qa.delete() + dn.delete() + + def test_delete_quality_inspection_linked_with_stock_entry(self): + item_code = create_item("_Test Cicuular Dependecy Item with QA").name + + se = make_stock_entry( + item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100, do_not_submit=True + ) + + se.inspection_required = 1 + se.save() + + qa = create_quality_inspection( + item_code=item_code, reference_type="Stock Entry", reference_name=se.name, do_not_submit=True + ) + + se.reload() + se.items[0].quality_inspection = qa.name + se.save() + + qa.delete() + + se.reload() + + qc = se.items[0].quality_inspection + self.assertFalse(qc) + + se.delete() + +>>>>>>> 7cc324e31e (fix: circular dependency error while deleting QC) def create_quality_inspection(**args): args = frappe._dict(args) From 453700d0abb59ca67234617d35301daa0640a2ab Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 8 Jan 2024 18:17:01 +0530 Subject: [PATCH 07/48] fix: incorrect percentage received in purchase invoice (cherry picked from commit 8d2c78867e1d563aafa740b3dc5f8a4fae68e52f) --- .../doctype/purchase_invoice/purchase_invoice.py | 12 ++++++++++++ .../doctype/purchase_receipt/purchase_receipt.py | 1 + 2 files changed, 13 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 4a9153a1ebc..2af11159298 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -131,6 +131,18 @@ class PurchaseInvoice(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + self.set_percentage_received() + + def set_percentage_received(self): + total_billed_qty = 0.0 + total_received_qty = 0.0 + for row in self.items: + if row.purchase_receipt and row.pr_detail and row.received_qty: + total_billed_qty += row.qty + total_received_qty += row.received_qty + + if total_billed_qty and total_received_qty: + self.per_received = total_received_qty / total_billed_qty * 100 def validate_release_date(self): if self.release_date and getdate(nowdate()) >= getdate(self.release_date): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d7cefe36473..d44bb4d26d8 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1077,6 +1077,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None): "field_map": { "name": "pr_detail", "parent": "purchase_receipt", + "qty": "received_qty", "purchase_order_item": "po_detail", "purchase_order": "purchase_order", "is_fixed_asset": "is_fixed_asset", From daf0e435e2522e54c815f9912ceca36e99e17f5d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 11 Jan 2024 14:53:21 +0530 Subject: [PATCH 08/48] chore: fix conflicts --- .../test_quality_inspection.py | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 5c734744bae..9eeb4602ab6 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -216,42 +216,6 @@ class TestQualityInspection(FrappeTestCase): qa.save() self.assertEqual(qa.status, "Accepted") -<<<<<<< HEAD -======= - @change_settings("System Settings", {"number_format": "#.###,##"}) - def test_diff_number_format(self): - self.assertEqual(frappe.db.get_default("number_format"), "#.###,##") # sanity check - - # Test QI based on acceptance values (Non formula) - dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) - readings = [ - { - "specification": "Iron Content", # numeric reading - "min_value": 60, - "max_value": 100, - "reading_1": "70,000", - }, - { - "specification": "Iron Content", # numeric reading - "min_value": 60, - "max_value": 100, - "reading_1": "1.100,00", - }, - ] - - qa = create_quality_inspection( - reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True - ) - - qa.save() - - # status must be auto set as per formula - self.assertEqual(qa.readings[0].status, "Accepted") - self.assertEqual(qa.readings[1].status, "Rejected") - - qa.delete() - dn.delete() - def test_delete_quality_inspection_linked_with_stock_entry(self): item_code = create_item("_Test Cicuular Dependecy Item with QA").name @@ -279,7 +243,6 @@ class TestQualityInspection(FrappeTestCase): se.delete() ->>>>>>> 7cc324e31e (fix: circular dependency error while deleting QC) def create_quality_inspection(**args): args = frappe._dict(args) From 298cdf5f0efe4e0a0e76157ad0719bee10822c4a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 11 Jan 2024 16:24:43 +0530 Subject: [PATCH 09/48] fix: broken dimension filters in Sales/Purchase register (cherry picked from commit 7b3f9386d7af54181a59c42e9e07ee3d6f54903e) --- erpnext/accounts/report/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 7dd61d2c64c..e27e45d439c 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -365,7 +365,7 @@ def filter_invoices_based_on_dimensions(filters, query, parent_doc): dimension.document_type, filters.get(dimension.fieldname) ) fieldname = dimension.fieldname - query = query.where(parent_doc[fieldname] == filters.fieldname) + query = query.where(parent_doc[fieldname].isin(filters[fieldname])) return query From 9395f7535b32b02806e7c36d4beb6b44a246a134 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 11 Jan 2024 16:45:49 +0530 Subject: [PATCH 10/48] fix: possible typeerror in consolidated report (cherry picked from commit 268731aec4fb7a8bf9bc698773894d8fa722eb1a) --- .../consolidated_financial_statement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index b7d25c41982..cb1083b06c1 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -129,7 +129,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { } value = default_formatter(value, row, column, data); - if (!data.parent_account) { + if (data && !data.parent_account) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); From 520cdb6f32f4477aa9fd0456c6b7c6328c5ccfe1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 8 Jan 2024 17:11:26 +0530 Subject: [PATCH 11/48] fix: project filters on Delivery Note and Sales Order (cherry picked from commit 9ba6ff67d5727ee97f12a9cbee120e78c20d6cec) --- erpnext/public/js/utils/dimension_tree_filter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index bb23f1512b9..3f70c09f667 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -16,6 +16,8 @@ erpnext.accounts.dimensions = { }, callback: function(r) { me.accounting_dimensions = r.message[0]; + // Ignoring "Project" as it is already handled specifically in Sales Order and Delivery Note + me.accounting_dimensions = me.accounting_dimensions.filter(x=>{return x.document_type != "Project"}); me.default_dimensions = r.message[1]; me.setup_filters(frm, doctype); } From 670d61547fb6a7ee86050c2f1020cd247f12df52 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 10 Jan 2024 22:05:02 +0530 Subject: [PATCH 12/48] fix: date in master document for dictionary condition (cherry picked from commit d96a777edd9cad698b9d9bb90863c1080eacb36f) # Conflicts: # erpnext/accounts/utils.py --- erpnext/accounts/utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 52b0c34673a..fd1e2914a4f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1222,8 +1222,18 @@ def get_autoname_with_number(number_value, doc_title, company): def parse_naming_series_variable(doc, variable): if variable == "FY": +<<<<<<< HEAD date = doc.get("posting_date") or doc.get("transaction_date") or getdate() return get_fiscal_year(date=date, company=doc.get("company"))[0] +======= + if doc: + date = doc.get("posting_date") or doc.get("transaction_date") or getdate() + company = doc.get("company") + else: + date = getdate() + company = None + return get_fiscal_year(date=date, company=company)[0] +>>>>>>> d96a777edd (fix: date in master document for dictionary condition) @frappe.whitelist() From d95b14d5b8f03c646a90186ce013af0cf42225b9 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 10 Jan 2024 22:06:06 +0530 Subject: [PATCH 13/48] test: naming series variable parsing (cherry picked from commit bbdf98a8f0a53b0b4c5a73eb1842401d67314760) --- erpnext/accounts/test/test_utils.py | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 3d5e5fc4ec7..3243e3dcc63 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -22,6 +22,9 @@ class TestUtils(unittest.TestCase): super(TestUtils, cls).setUpClass() make_test_objects("Address", ADDRESS_RECORDS) + def tearDown(self): + frappe.db.rollback() + def test_get_party_shipping_address(self): address = get_party_shipping_address("Customer", "_Test Customer 1") self.assertEqual(address, "_Test Billing Address 2 Title-Billing") @@ -125,6 +128,38 @@ class TestUtils(unittest.TestCase): self.assertEqual(len(payment_entry.references), 1) self.assertEqual(payment_entry.difference_amount, 0) + def test_naming_series_variable_parsing(self): + """ + Tests parsing utility used by Naming Series Variable hook for FY + """ + from frappe.custom.doctype.property_setter.property_setter import make_property_setter + from frappe.utils import nowdate + + from erpnext.accounts.utils import get_fiscal_year + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + # Configure Supplier Naming in Buying Settings + frappe.db.set_default("supp_master_name", "Auto Name") + + # Configure Autoname in Supplier DocType + make_property_setter( + "Supplier", None, "naming_rule", "Expression", "Data", for_doctype="Doctype" + ) + make_property_setter( + "Supplier", None, "autoname", "SUP-.FY.-.#####", "Data", for_doctype="Doctype" + ) + + # Create Fiscal Year for Current Year + fiscal_year = get_fiscal_year(nowdate())[0] + + # Create Supplier + supplier = create_supplier() + + # Check Naming Series in generated Supplier ID + doc_name = supplier.name.split("-") + self.assertEqual(len(doc_name), 3) + self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year)) + ADDRESS_RECORDS = [ { From 6bd01f227eea82f434efa1e3bdb4f9ab2a3e0f26 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 11 Jan 2024 12:22:08 +0530 Subject: [PATCH 14/48] fix: reset default after test (cherry picked from commit 813b7a96fbb0ff048c112dec5551edf5980d0955) --- erpnext/accounts/test/test_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 3243e3dcc63..660cb62d2c5 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -22,7 +22,8 @@ class TestUtils(unittest.TestCase): super(TestUtils, cls).setUpClass() make_test_objects("Address", ADDRESS_RECORDS) - def tearDown(self): + @classmethod + def tearDownClass(cls): frappe.db.rollback() def test_get_party_shipping_address(self): @@ -149,7 +150,6 @@ class TestUtils(unittest.TestCase): "Supplier", None, "autoname", "SUP-.FY.-.#####", "Data", for_doctype="Doctype" ) - # Create Fiscal Year for Current Year fiscal_year = get_fiscal_year(nowdate())[0] # Create Supplier @@ -159,6 +159,7 @@ class TestUtils(unittest.TestCase): doc_name = supplier.name.split("-") self.assertEqual(len(doc_name), 3) self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year)) + frappe.db.set_default("supp_master_name", "Supplier Name") ADDRESS_RECORDS = [ From 318026615036dfdc6bb169788e25ae0f9a4520e1 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 3 Jan 2024 13:39:28 +0100 Subject: [PATCH 15/48] fix: consider all years in holiday list (cherry picked from commit 300aaa39fecf5b65fadf9ec18d122b6d8e327fca) --- erpnext/setup/doctype/holiday_list/holiday_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 526bc2ba4ac..4593cbda5ee 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -63,7 +63,7 @@ class HolidayList(Document): for holiday_date, holiday_name in country_holidays( self.country, subdiv=self.subdivision, - years=[from_date.year, to_date.year], + years=list(range(from_date.year, to_date.year + 1)), language=frappe.local.lang, ).items(): if holiday_date in existing_holidays: From f3d8d273f4daba82ee61bf6b4a60707e77a5ed11 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 4 Jan 2024 11:04:25 +0100 Subject: [PATCH 16/48] test: improve test for local holidays (cherry picked from commit 60329ade9e4377741ef1578b4a9cc9529e3766c1) --- .../doctype/holiday_list/test_holiday_list.py | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/erpnext/setup/doctype/holiday_list/test_holiday_list.py b/erpnext/setup/doctype/holiday_list/test_holiday_list.py index 7eeb27d864e..c0e71f5d254 100644 --- a/erpnext/setup/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/test_holiday_list.py @@ -48,17 +48,58 @@ class TestHolidayList(unittest.TestCase): def test_local_holidays(self): holiday_list = frappe.new_doc("Holiday List") - holiday_list.from_date = "2023-04-01" - holiday_list.to_date = "2023-04-30" + holiday_list.from_date = "2022-01-01" + holiday_list.to_date = "2024-12-31" holiday_list.country = "DE" holiday_list.subdivision = "SN" holiday_list.get_local_holidays() - holidays = [holiday.holiday_date for holiday in holiday_list.holidays] - self.assertNotIn(date(2023, 1, 1), holidays) + holidays = holiday_list.get_holidays() + self.assertIn(date(2022, 1, 1), holidays) + self.assertIn(date(2022, 4, 15), holidays) + self.assertIn(date(2022, 4, 18), holidays) + self.assertIn(date(2022, 5, 1), holidays) + self.assertIn(date(2022, 5, 26), holidays) + self.assertIn(date(2022, 6, 6), holidays) + self.assertIn(date(2022, 10, 3), holidays) + self.assertIn(date(2022, 10, 31), holidays) + self.assertIn(date(2022, 11, 16), holidays) + self.assertIn(date(2022, 12, 25), holidays) + self.assertIn(date(2022, 12, 26), holidays) + self.assertIn(date(2023, 1, 1), holidays) self.assertIn(date(2023, 4, 7), holidays) self.assertIn(date(2023, 4, 10), holidays) - self.assertNotIn(date(2023, 5, 1), holidays) + self.assertIn(date(2023, 5, 1), holidays) + self.assertIn(date(2023, 5, 18), holidays) + self.assertIn(date(2023, 5, 29), holidays) + self.assertIn(date(2023, 10, 3), holidays) + self.assertIn(date(2023, 10, 31), holidays) + self.assertIn(date(2023, 11, 22), holidays) + self.assertIn(date(2023, 12, 25), holidays) + self.assertIn(date(2023, 12, 26), holidays) + self.assertIn(date(2024, 1, 1), holidays) + self.assertIn(date(2024, 3, 29), holidays) + self.assertIn(date(2024, 4, 1), holidays) + self.assertIn(date(2024, 5, 1), holidays) + self.assertIn(date(2024, 5, 9), holidays) + self.assertIn(date(2024, 5, 20), holidays) + self.assertIn(date(2024, 10, 3), holidays) + self.assertIn(date(2024, 10, 31), holidays) + self.assertIn(date(2024, 11, 20), holidays) + self.assertIn(date(2024, 12, 25), holidays) + self.assertIn(date(2024, 12, 26), holidays) + + # check some random dates that should not be local holidays + self.assertNotIn(date(2022, 1, 2), holidays) + self.assertNotIn(date(2023, 4, 16), holidays) + self.assertNotIn(date(2024, 4, 19), holidays) + self.assertNotIn(date(2022, 5, 2), holidays) + self.assertNotIn(date(2023, 5, 27), holidays) + self.assertNotIn(date(2024, 6, 7), holidays) + self.assertNotIn(date(2022, 10, 4), holidays) + self.assertNotIn(date(2023, 10, 30), holidays) + self.assertNotIn(date(2024, 11, 17), holidays) + self.assertNotIn(date(2022, 12, 24), holidays) def test_localized_country_names(self): lang = frappe.local.lang From 755576bd78be12ffc0c6584cd659cc7279624fb0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 27 Dec 2023 23:05:37 +0100 Subject: [PATCH 17/48] fix: unreconcile Bank Transaction on cancel of payment voucher (cherry picked from commit 0a95b38166e36946f89286d39081b0f529e88cb6) # Conflicts: # erpnext/accounts/doctype/bank_transaction/bank_transaction.py # erpnext/accounts/doctype/journal_entry/journal_entry.js --- .../bank_transaction/bank_transaction.py | 23 +++++++++++++++++++ .../doctype/journal_entry/journal_entry.js | 4 ++++ .../doctype/payment_entry/payment_entry.js | 2 +- .../purchase_invoice/purchase_invoice.js | 2 +- .../doctype/sales_invoice/sales_invoice.js | 2 +- erpnext/controllers/accounts_controller.py | 5 ++++ 6 files changed, 35 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 256bde5c719..54c4655d264 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -2,6 +2,11 @@ # For license information, please see license.txt import frappe +<<<<<<< HEAD +======= +from frappe import _ +from frappe.model.docstatus import DocStatus +>>>>>>> 0a95b38166 (fix: unreconcile Bank Transaction on cancel of payment voucher) from frappe.utils import flt from erpnext.controllers.status_updater import StatusUpdater @@ -393,3 +398,21 @@ def unclear_reference_payment(doctype, docname, bt_name): bt = frappe.get_doc("Bank Transaction", bt_name) set_voucher_clearance(doctype, docname, None, bt) return docname + + +def remove_from_bank_transaction(doctype, docname): + """Remove a (cancelled) voucher from all Bank Transactions.""" + for bt_name in get_reconciled_bank_transactions(doctype, docname): + bt = frappe.get_doc("Bank Transaction", bt_name) + if bt.docstatus == DocStatus.cancelled(): + continue + + modified = False + + for pe in bt.payment_entries: + if pe.payment_document == doctype and pe.payment_entry == docname: + bt.remove(pe) + modified = True + + if modified: + bt.save() diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index c59643280e8..bf71a4c6fa5 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,11 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); +<<<<<<< HEAD frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; +======= + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"]; +>>>>>>> 0a95b38166 (fix: unreconcile Bank Transaction on cancel of payment voucher) }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index d5eeae5ab34..8ed2654c7f9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries', "Bank Transaction"]; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 3b77614607a..0add6c57da6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. super.onload(); // Ignore linked advances - this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"]; if(!this.frm.doc.__islocal) { // show credit_to in print format diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index b236b447d57..1e8428d9e29 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e580748866d..cc8ee9226a4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1347,11 +1347,16 @@ class AccountsController(TransactionBase): reconcile_against_document(lst) def on_cancel(self): + from erpnext.accounts.doctype.bank_transaction.bank_transaction import ( + remove_from_bank_transaction, + ) from erpnext.accounts.utils import ( cancel_exchange_gain_loss_journal, unlink_ref_doc_from_payment_entries, ) + remove_from_bank_transaction(self.doctype, self.name) + if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: # Cancel Exchange Gain/Loss Journal before unlinking cancel_exchange_gain_loss_journal(self) From 4a1a4b06dd320f9f76ed6f2374258815fd1add7e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 28 Dec 2023 00:00:04 +0100 Subject: [PATCH 18/48] test: cancel voucher linked to Bank Transaction (cherry picked from commit 517bedeb7ee68d7794d09a5f49b97ab64f652abf) --- .../bank_transaction/test_bank_transaction.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index f900e0775ce..eb0dc74825d 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -2,10 +2,10 @@ # See license.txt import json -import unittest import frappe from frappe import utils +from frappe.model.docstatus import DocStatus from frappe.tests.utils import FrappeTestCase from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import ( @@ -81,6 +81,29 @@ class TestBankTransaction(FrappeTestCase): clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") self.assertFalse(clearance_date) + def test_cancel_voucher(self): + bank_transaction = frappe.get_doc( + "Bank Transaction", + dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"), + ) + payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700)) + vouchers = json.dumps( + [ + { + "payment_doctype": "Payment Entry", + "payment_name": payment.name, + "amount": bank_transaction.unallocated_amount, + } + ] + ) + reconcile_vouchers(bank_transaction.name, vouchers) + payment.reload() + payment.cancel() + bank_transaction.reload() + self.assertEqual(bank_transaction.docstatus, DocStatus.submitted()) + self.assertEqual(bank_transaction.unallocated_amount, 1700) + self.assertEqual(bank_transaction.payment_entries, []) + # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount def test_debit_credit_output(self): bank_transaction = frappe.get_doc( From 4998d68564c5eb1d99bd39a1f7ce21e7f519683d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:04:01 +0100 Subject: [PATCH 19/48] chore: resolve merge confilcts --- .../accounts/doctype/bank_transaction/bank_transaction.py | 6 +----- erpnext/accounts/doctype/journal_entry/journal_entry.js | 6 +----- erpnext/accounts/doctype/payment_entry/payment_entry.js | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 54c4655d264..86c3a8a9336 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -2,11 +2,7 @@ # For license information, please see license.txt import frappe -<<<<<<< HEAD -======= -from frappe import _ from frappe.model.docstatus import DocStatus ->>>>>>> 0a95b38166 (fix: unreconcile Bank Transaction on cancel of payment voucher) from frappe.utils import flt from erpnext.controllers.status_updater import StatusUpdater @@ -73,7 +69,7 @@ class BankTransaction(StatusUpdater): "payment_entry": voucher["payment_name"], "allocated_amount": 0.0, # Temporary } - child = self.append("payment_entries", pe) + self.append("payment_entries", pe) added = True # runs on_update_after_submit diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index bf71a4c6fa5..f86320d917a 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,11 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); -<<<<<<< HEAD - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; -======= - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"]; ->>>>>>> 0a95b38166 (fix: unreconcile Bank Transaction on cancel of payment voucher) + frm.ignore_doctypes_on_cancel_all = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Repost Payment Ledger", "Asset", "Asset Movement", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"]; }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 8ed2654c7f9..6b3f46d3833 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries', "Bank Transaction"]; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries', 'Bank Transaction']; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); From 7ae1df60be0a0e1e9af292e9abed0e2cb7a3ec63 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 16:48:50 +0530 Subject: [PATCH 20/48] chore: remove share, print and email permissions from Buying Settings (backport #39337) (#39338) chore: remove share, print and email permissions from Buying Settings (cherry picked from commit 3c46abca6c362f8ddd7d269d8f28ee07ee557cc8) Co-authored-by: s-aga-r --- .../buying_settings/buying_settings.json | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 5be70288c56..f070c40a589 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -188,7 +188,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-01-05 15:26:02.320942", + "modified": "2024-01-12 16:42:01.894346", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -214,39 +214,24 @@ "write": 1 }, { - "email": 1, - "print": 1, "read": 1, - "role": "Accounts User", - "share": 1 + "role": "Accounts User" }, { - "email": 1, - "print": 1, "read": 1, - "role": "Accounts Manager", - "share": 1 + "role": "Accounts Manager" }, { - "email": 1, - "print": 1, "read": 1, - "role": "Stock Manager", - "share": 1 + "role": "Stock Manager" }, { - "email": 1, - "print": 1, "read": 1, - "role": "Stock User", - "share": 1 + "role": "Stock User" }, { - "email": 1, - "print": 1, "read": 1, - "role": "Purchase User", - "share": 1 + "role": "Purchase User" } ], "sort_field": "modified", From d21fc6055cc25a1d4af6c290c2584e3de30fffc5 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 27 Dec 2023 19:32:48 +0530 Subject: [PATCH 21/48] fix: use child table values instead of global min max (cherry picked from commit 43fed29514c0d839312f33e1d7d490ae73d9830f) --- erpnext/stock/doctype/item/item.js | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index b306a41bb83..1b71017bd4d 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -630,26 +630,12 @@ $.extend(erpnext.item, { } }); } else { - frappe.call({ - method: "frappe.client.get", - args: { - doctype: "Item Attribute", - name: d.attribute - } - }).then((r) => { - if(r.message) { - const from = r.message.from_range; - const to = r.message.to_range; - const increment = r.message.increment; - - let values = []; - for(var i = from; i <= to; i = flt(i + increment, 6)) { - values.push(i); - } - attr_val_fields[d.attribute] = values; - resolve(); - } - }); + let values = []; + for(var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) { + values.push(i); + } + attr_val_fields[d.attribute] = values; + resolve(); } }); From d9f7070f92216c2422f4e1ad262e9397fb940969 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 12 Jan 2024 14:11:37 +0530 Subject: [PATCH 22/48] fix: added indexing to improve performance (cherry picked from commit ac81323fec7287a986feaf7ba496395bf3f6a424) # Conflicts: # erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py --- .../doctype/stock_entry/stock_entry.json | 8 ++- .../stock_entry_detail.json | 5 +- .../stock_entry_detail/stock_entry_detail.py | 63 +++++++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 564c380017b..d45296f1310 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -104,7 +104,8 @@ "in_standard_filter": 1, "label": "Stock Entry Type", "options": "Stock Entry Type", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "depends_on": "eval:doc.purpose == 'Material Transfer'", @@ -546,7 +547,8 @@ "label": "Job Card", "options": "Job Card", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "amended_from", @@ -679,7 +681,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-06-19 18:23:40.748114", + "modified": "2024-01-12 11:56:58.644882", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 6b1a8efc997..5e523aef999 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -557,7 +557,8 @@ "label": "Job Card Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "0", @@ -572,7 +573,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-05-09 12:41:18.210864", + "modified": "2024-01-12 11:56:04.626103", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py index 000ff2dcf8b..d7250b23069 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py @@ -6,4 +6,67 @@ from frappe.model.document import Document class StockEntryDetail(Document): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + actual_qty: DF.Float + additional_cost: DF.Currency + against_stock_entry: DF.Link | None + allow_alternative_item: DF.Check + allow_zero_valuation_rate: DF.Check + amount: DF.Currency + barcode: DF.Data | None + basic_amount: DF.Currency + basic_rate: DF.Currency + batch_no: DF.Link | None + bom_no: DF.Link | None + conversion_factor: DF.Float + cost_center: DF.Link | None + description: DF.TextEditor | None + expense_account: DF.Link | None + has_item_scanned: DF.Check + image: DF.Attach | None + is_finished_item: DF.Check + is_scrap_item: DF.Check + item_code: DF.Link + item_group: DF.Data | None + item_name: DF.Data | None + job_card_item: DF.Data | None + material_request: DF.Link | None + material_request_item: DF.Link | None + original_item: DF.Link | None + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + po_detail: DF.Data | None + project: DF.Link | None + putaway_rule: DF.Link | None + qty: DF.Float + quality_inspection: DF.Link | None + reference_purchase_receipt: DF.Link | None + retain_sample: DF.Check + s_warehouse: DF.Link | None + sample_quantity: DF.Int + sco_rm_detail: DF.Data | None + serial_and_batch_bundle: DF.Link | None + serial_no: DF.SmallText | None + set_basic_rate_manually: DF.Check + ste_detail: DF.Data | None + stock_uom: DF.Link + subcontracted_item: DF.Link | None + t_warehouse: DF.Link | None + transfer_qty: DF.Float + transferred_qty: DF.Float + uom: DF.Link + valuation_rate: DF.Currency + # end: auto-generated types + +>>>>>>> ac81323fec (fix: added indexing to improve performance) pass From c2fee6c25dc16cf44b3da930254c70ad01d9e9a3 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 12 Jan 2024 20:56:40 +0530 Subject: [PATCH 23/48] chore: fix conflicts --- .../stock_entry_detail/stock_entry_detail.py | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py index d7250b23069..000ff2dcf8b 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py @@ -6,67 +6,4 @@ from frappe.model.document import Document class StockEntryDetail(Document): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - actual_qty: DF.Float - additional_cost: DF.Currency - against_stock_entry: DF.Link | None - allow_alternative_item: DF.Check - allow_zero_valuation_rate: DF.Check - amount: DF.Currency - barcode: DF.Data | None - basic_amount: DF.Currency - basic_rate: DF.Currency - batch_no: DF.Link | None - bom_no: DF.Link | None - conversion_factor: DF.Float - cost_center: DF.Link | None - description: DF.TextEditor | None - expense_account: DF.Link | None - has_item_scanned: DF.Check - image: DF.Attach | None - is_finished_item: DF.Check - is_scrap_item: DF.Check - item_code: DF.Link - item_group: DF.Data | None - item_name: DF.Data | None - job_card_item: DF.Data | None - material_request: DF.Link | None - material_request_item: DF.Link | None - original_item: DF.Link | None - parent: DF.Data - parentfield: DF.Data - parenttype: DF.Data - po_detail: DF.Data | None - project: DF.Link | None - putaway_rule: DF.Link | None - qty: DF.Float - quality_inspection: DF.Link | None - reference_purchase_receipt: DF.Link | None - retain_sample: DF.Check - s_warehouse: DF.Link | None - sample_quantity: DF.Int - sco_rm_detail: DF.Data | None - serial_and_batch_bundle: DF.Link | None - serial_no: DF.SmallText | None - set_basic_rate_manually: DF.Check - ste_detail: DF.Data | None - stock_uom: DF.Link - subcontracted_item: DF.Link | None - t_warehouse: DF.Link | None - transfer_qty: DF.Float - transferred_qty: DF.Float - uom: DF.Link - valuation_rate: DF.Currency - # end: auto-generated types - ->>>>>>> ac81323fec (fix: added indexing to improve performance) pass From 1a26c70df285176dc282426bc715b83c470075e5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 12 Jan 2024 18:28:52 +0530 Subject: [PATCH 24/48] fix: incorrect active serial nos due to backdated transactions --- .../purchase_receipt/test_purchase_receipt.py | 79 ---------------- .../stock_reconciliation.py | 58 ++++++++---- .../test_stock_reconciliation.py | 68 ++++++++++++++ erpnext/stock/stock_ledger.py | 94 +++++++++++-------- erpnext/stock/utils.py | 4 + 5 files changed, 164 insertions(+), 139 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 404758ce94f..b444e864e3d 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -13,7 +13,6 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse -from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction class TestPurchaseReceipt(FrappeTestCase): @@ -197,84 +196,6 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name})) self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no})) - def test_duplicate_serial_nos(self): - from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - - item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"}) - if not item: - item = create_item("Test Serialized Item 123") - item.has_serial_no = 1 - item.serial_no_series = "TSI123-.####" - item.save() - else: - item = frappe.get_doc("Item", {"item_name": "Test Serialized Item 123"}) - - # First make purchase receipt - pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500) - pr.load_from_db() - - serial_nos = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name}, - "serial_no", - ) - - serial_nos = get_serial_nos(serial_nos) - - self.assertEquals(get_serial_nos(pr.items[0].serial_no), serial_nos) - - # Then tried to receive same serial nos in difference company - pr_different_company = make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - serial_no="\n".join(serial_nos), - company="_Test Company 1", - do_not_submit=True, - warehouse="Stores - _TC1", - ) - - self.assertRaises(SerialNoDuplicateError, pr_different_company.submit) - - # Then made delivery note to remove the serial nos from stock - dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no="\n".join(serial_nos)) - dn.load_from_db() - self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos) - - posting_date = add_days(today(), -3) - - # Try to receive same serial nos again in the same company with backdated. - pr1 = make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - posting_date=posting_date, - serial_no="\n".join(serial_nos), - do_not_submit=True, - ) - - self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit) - - # Try to receive same serial nos with different company with backdated. - pr2 = make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - posting_date=posting_date, - serial_no="\n".join(serial_nos), - company="_Test Company 1", - do_not_submit=True, - warehouse="Stores - _TC1", - ) - - self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit) - - # Receive the same serial nos after the delivery note posting date and time - make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no="\n".join(serial_nos)) - - # Raise the error for backdated deliver note entry cancel - self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel) - def test_purchase_receipt_gl_entry(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index dd39f103cd7..9a46ae71ad2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -634,30 +634,48 @@ class StockReconciliation(StockController): if voucher_detail_no != row.name: continue - current_qty = get_batch_qty_for_stock_reco( - row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name - ) + if row.serial_no: + item_dict = get_stock_balance_for( + row.item_code, + row.warehouse, + self.posting_date, + self.posting_time, + voucher_no=self.name, + ) + + current_qty = item_dict.get("qty") + row.current_serial_no = item_dict.get("serial_nos") + row.current_valuation_rate = item_dict.get("rate") + else: + current_qty = get_batch_qty_for_stock_reco( + row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name + ) precesion = row.precision("current_qty") if flt(current_qty, precesion) != flt(row.current_qty, precesion): - val_rate = get_valuation_rate( - row.item_code, - row.warehouse, - self.doctype, - self.name, - company=self.company, - batch_no=row.batch_no, - ) + if not row.serial_no: + val_rate = get_valuation_rate( + row.item_code, + row.warehouse, + self.doctype, + self.name, + company=self.company, + batch_no=row.batch_no, + ) + + row.current_valuation_rate = val_rate - row.current_valuation_rate = val_rate row.current_qty = current_qty - 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), - } - ) + values_to_update = { + "current_qty": row.current_qty, + "current_valuation_rate": row.current_valuation_rate, + "current_amount": flt(row.current_qty * row.current_valuation_rate), + } + + if row.current_serial_no: + values_to_update["current_serial_no"] = row.current_serial_no + + row.db_set(values_to_update) if ( add_new_sle @@ -880,6 +898,7 @@ def get_stock_balance_for( batch_no: Optional[str] = None, with_valuation_rate: bool = True, inventory_dimensions_dict=None, + voucher_no=None, ): frappe.has_permission("Stock Reconciliation", "write", throw=True) @@ -910,6 +929,7 @@ def get_stock_balance_for( with_serial_no=has_serial_no, inventory_dimensions_dict=inventory_dimensions_dict, batch_no=batch_no, + voucher_no=voucher_no, ) if has_serial_no: diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 05c60175f51..19c04afe909 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1055,6 +1055,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(sr.items[0].current_qty, se2.items[0].qty) self.assertEqual(len(sr.items[0].current_serial_no.split("\n")), sr.items[0].current_qty) + def test_backdated_purchase_receipt_with_stock_reco(self): + item_code = self.make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "TEST-SERIAL-.###", + } + ).name + + warehouse = "_Test Warehouse - _TC" + + # Step - 1: Create a Backdated Purchase Receipt + + pr1 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) + ) + pr1.reload() + + serial_nos = sorted(get_serial_nos(pr1.items[0].serial_no))[:5] + + # Step - 2: Create a Stock Reconciliation + sr1 = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=5, + serial_no="\n".join(serial_nos), + ) + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["serial_no", "actual_qty", "stock_value_difference"], + filters={"voucher_no": sr1.name, "is_cancelled": 0}, + order_by="creation", + ) + + for d in data: + if d.actual_qty < 0: + self.assertEqual(d.actual_qty, -10.0) + self.assertAlmostEqual(d.stock_value_difference, -1000.0) + else: + self.assertEqual(d.actual_qty, 5.0) + self.assertAlmostEqual(d.stock_value_difference, 500.0) + + # Step - 3: Create a Purchase Receipt before the first Purchase Receipt + make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5) + ) + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["serial_no", "actual_qty", "stock_value_difference"], + filters={"voucher_no": sr1.name, "is_cancelled": 0}, + order_by="creation", + ) + + for d in data: + if d.actual_qty < 0: + self.assertEqual(d.actual_qty, -20.0) + self.assertAlmostEqual(d.stock_value_difference, -3000.0) + else: + self.assertEqual(d.actual_qty, 5.0) + self.assertAlmostEqual(d.stock_value_difference, 500.0) + + active_serial_no = frappe.get_all( + "Serial No", filters={"status": "Active", "item_code": item_code} + ) + self.assertEqual(len(active_serial_no), 5) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index df1f544d7b1..ef1b0cda4ff 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1,7 +1,6 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -import copy import json from typing import Optional, Set, Tuple @@ -27,10 +26,6 @@ class NegativeStockError(frappe.ValidationError): pass -class SerialNoExistsInFutureTransaction(frappe.ValidationError): - pass - - def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): """Create SL entries from SL entry dicts @@ -54,9 +49,6 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc future_sle_exists(args, sl_entries) for sle in sl_entries: - if sle.serial_no and not via_landed_cost_voucher: - validate_serial_no(sle) - if cancel: sle["actual_qty"] = -flt(sle.get("actual_qty")) @@ -133,35 +125,6 @@ def get_args_for_future_sle(row): ) -def validate_serial_no(sle): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - - for sn in get_serial_nos(sle.serial_no): - args = copy.deepcopy(sle) - args.serial_no = sn - args.warehouse = "" - - vouchers = [] - for row in get_stock_ledger_entries(args, ">"): - voucher_type = frappe.bold(row.voucher_type) - voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no)) - vouchers.append(f"{voucher_type} {voucher_no}") - - if vouchers: - serial_no = frappe.bold(sn) - msg = ( - f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first. - The list of the transactions are as below.""" - + "

  • " - ) - - msg += "
  • ".join(vouchers) - msg += "
" - - title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel" - frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction) - - def validate_cancellation(args): if args[0].get("is_cancelled"): repost_entry = frappe.db.get_value( @@ -573,7 +536,12 @@ class update_entries_after(object): if not self.args.get("sle_id"): self.get_dynamic_incoming_outgoing_rate(sle) - if sle.voucher_type == "Stock Reconciliation" and sle.batch_no and sle.voucher_detail_no: + if ( + sle.voucher_type == "Stock Reconciliation" + and not self.args.get("sle_id") + and sle.voucher_detail_no + and (sle.batch_no or sle.serial_no) + ): self.reset_actual_qty_for_stock_reco(sle) if ( @@ -651,11 +619,52 @@ class update_entries_after(object): doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty >= 0) if sle.actual_qty < 0: - sle.actual_qty = ( - flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty")) - * -1 + stock_reco_details = frappe.db.get_value( + "Stock Reconciliation Item", + sle.voucher_detail_no, + ["current_qty", "current_serial_no as sn_no"], + as_dict=True, ) + sle.actual_qty = flt(stock_reco_details.current_qty) * -1 + + if stock_reco_details.sn_no: + sle.serial_no = stock_reco_details.sn_no + sle.qty_after_transaction = 0.0 + + if sle.serial_no: + self.update_serial_no_status(sle) + + def update_serial_no_status(self, sle): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + serial_nos = get_serial_nos(sle.serial_no) + warehouse = None + status = "Delivered" + if sle.actual_qty > 0: + warehouse = sle.warehouse + status = "Active" + + sn_table = frappe.qb.DocType("Serial No") + + query = ( + frappe.qb.update(sn_table) + .set(sn_table.warehouse, warehouse) + .set(sn_table.status, status) + .where(sn_table.name.isin(serial_nos)) + ) + + if sle.actual_qty > 0: + query = query.set(sn_table.purchase_document_type, sle.voucher_type) + query = query.set(sn_table.purchase_document_no, sle.voucher_no) + query = query.set(sn_table.delivery_document_type, None) + query = query.set(sn_table.delivery_document_no, None) + else: + query = query.set(sn_table.delivery_document_type, sle.voucher_type) + query = query.set(sn_table.delivery_document_no, sle.voucher_no) + + query.run() + def validate_negative_stock(self, sle): """ validate negative stock for entries current datetime onwards @@ -1282,6 +1291,9 @@ def get_stock_ledger_entries( if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" + if operator in (">", "<=") and previous_sle.get("voucher_no"): + conditions += " and voucher_no!=%(voucher_no)s" + if extra_cond: conditions += f"{extra_cond}" diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index e019c572daf..2b57a1be8fa 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -96,6 +96,7 @@ def get_stock_balance( with_serial_no=False, inventory_dimensions_dict=None, batch_no=None, + voucher_no=None, ): """Returns stock balance quantity at given warehouse on given posting date or current date. @@ -115,6 +116,9 @@ def get_stock_balance( "posting_time": posting_time, } + if voucher_no: + args["voucher_no"] = voucher_no + extra_cond = "" if inventory_dimensions_dict: for field, value in inventory_dimensions_dict.items(): From 1acaa20ee11056c9f79cab333474bf13dfb56bf6 Mon Sep 17 00:00:00 2001 From: RJPvT <48353029+RJPvT@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:09:11 +0100 Subject: [PATCH 25/48] fix: empty category in Plaid --- .../doctype/plaid_settings/plaid_settings.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index a462141a86b..5354d0d6c13 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -239,11 +239,12 @@ def new_bank_transaction(transaction): withdrawal = 0.0 tags = [] - try: - tags += transaction["category"] - tags += [f'Plaid Cat. {transaction["category_id"]}'] - except KeyError: - pass + if transaction["category"]: + try: + tags += transaction["category"] + tags += [f'Plaid Cat. {transaction["category_id"]}'] + except KeyError: + pass if not frappe.db.exists( "Bank Transaction", dict(transaction_id=transaction["transaction_id"]) From 5336cd49c90ede14fd3cb525444da2f7dcb4b3f5 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 14 Jan 2024 09:41:48 +0530 Subject: [PATCH 26/48] chore: fix conflicts --- erpnext/accounts/utils.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index fd1e2914a4f..d9fb75a86d2 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1222,10 +1222,6 @@ def get_autoname_with_number(number_value, doc_title, company): def parse_naming_series_variable(doc, variable): if variable == "FY": -<<<<<<< HEAD - date = doc.get("posting_date") or doc.get("transaction_date") or getdate() - return get_fiscal_year(date=date, company=doc.get("company"))[0] -======= if doc: date = doc.get("posting_date") or doc.get("transaction_date") or getdate() company = doc.get("company") @@ -1233,7 +1229,6 @@ def parse_naming_series_variable(doc, variable): date = getdate() company = None return get_fiscal_year(date=date, company=company)[0] ->>>>>>> d96a777edd (fix: date in master document for dictionary condition) @frappe.whitelist() From 664abc628742e9fb4e17eabb5b32f685bdce620c Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:47:44 +0100 Subject: [PATCH 27/48] Update sales_taxes_and_charges.json Change Rate label to existing Tax Rate label so it can be correctly translated in other languages (cherry picked from commit 2b93be1139bcb963a89c10f261a1dd2c83f01980) --- .../sales_taxes_and_charges/sales_taxes_and_charges.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index e236577e118..527e4c866ba 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -108,7 +108,7 @@ "fieldname": "rate", "fieldtype": "Float", "in_list_view": 1, - "label": "Rate", + "label": "Tax Rate", "oldfieldname": "rate", "oldfieldtype": "Currency" }, @@ -227,4 +227,4 @@ "sort_field": "modified", "sort_order": "ASC", "states": [] -} \ No newline at end of file +} From cb6757437ed72377ece6c0ded858019ce2b087be Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 14 Jan 2024 09:48:05 +0530 Subject: [PATCH 28/48] fix: modified date was not updated (cherry picked from commit f567af49a697568ba1d5dc3507953c332cb1a60a) --- .../sales_taxes_and_charges/sales_taxes_and_charges.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index 527e4c866ba..f9e5f4129c9 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -218,7 +218,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-10-17 13:08:17.776528", + "modified": "2022-10-18 13:08:17.776528", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges", From c69a59c3c6c72ba69a8347b2c2940e2aaa49428e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 14 Jan 2024 10:05:26 +0530 Subject: [PATCH 29/48] fix: added item group in stock reco (cherry picked from commit 116ff8241caeb98df28c91143a076dfa9e3179d6) # Conflicts: # erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json --- .../stock_reconciliation_item.json | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index a04309ad48e..0c0d7e87379 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -10,8 +10,9 @@ "has_item_scanned", "item_code", "item_name", - "warehouse", + "item_group", "column_break_6", + "warehouse", "qty", "valuation_rate", "amount", @@ -49,6 +50,7 @@ "reqd": 1 }, { + "fetch_from": "item_code.item_name", "fieldname": "item_name", "fieldtype": "Data", "in_global_search": 1, @@ -186,11 +188,47 @@ "fieldtype": "Data", "label": "Has Item Scanned", "read_only": 1 +<<<<<<< HEAD +======= + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial / Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1, + "search_index": 1 + }, + { + "fieldname": "current_serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Current Serial / Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "read_only": 1 + }, + { + "fieldname": "add_serial_batch_bundle", + "fieldtype": "Button", + "label": "Add Serial / Batch No" + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" +>>>>>>> 116ff8241c (fix: added item group in stock reco) } ], "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-07-25 11:58:44.992419", +======= + "modified": "2024-01-14 10:04:23.599951", +>>>>>>> 116ff8241c (fix: added item group in stock reco) "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", From 4d11a9c884f2b6d0940e43de4f1ee75add03a202 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 14 Jan 2024 10:32:10 +0530 Subject: [PATCH 30/48] chore: fix conflicts --- .../stock_reconciliation_item.json | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index 0c0d7e87379..e3cd34725b8 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -188,30 +188,6 @@ "fieldtype": "Data", "label": "Has Item Scanned", "read_only": 1 -<<<<<<< HEAD -======= - }, - { - "fieldname": "serial_and_batch_bundle", - "fieldtype": "Link", - "label": "Serial / Batch Bundle", - "no_copy": 1, - "options": "Serial and Batch Bundle", - "print_hide": 1, - "search_index": 1 - }, - { - "fieldname": "current_serial_and_batch_bundle", - "fieldtype": "Link", - "label": "Current Serial / Batch Bundle", - "no_copy": 1, - "options": "Serial and Batch Bundle", - "read_only": 1 - }, - { - "fieldname": "add_serial_batch_bundle", - "fieldtype": "Button", - "label": "Add Serial / Batch No" }, { "fetch_from": "item_code.item_group", @@ -219,16 +195,11 @@ "fieldtype": "Link", "label": "Item Group", "options": "Item Group" ->>>>>>> 116ff8241c (fix: added item group in stock reco) } ], "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-07-25 11:58:44.992419", -======= "modified": "2024-01-14 10:04:23.599951", ->>>>>>> 116ff8241c (fix: added item group in stock reco) "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", @@ -239,4 +210,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From 28434d101b381b03475f61b222043df17a2aea51 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 14 Jan 2024 10:44:18 +0530 Subject: [PATCH 31/48] fix: modified date --- .../sales_taxes_and_charges/sales_taxes_and_charges.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index f9e5f4129c9..9e0a7983b74 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -218,7 +218,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-10-18 13:08:17.776528", + "modified": "2024-01-14 10:08:17.776528", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges", From e9af0c6e675922e4534fe51a2d7b8266283f3c51 Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:45:39 +0100 Subject: [PATCH 32/48] Update purchase_taxes_and_charges.json label Rate to Tax Rate Change Rate label to existing Tax Rate label so it can be correctly translated in other languages (cherry picked from commit bd464197c41329ddf3cff50cd3eb876df9c6c382) --- .../purchase_taxes_and_charges.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 347cae05b72..249e7518f0b 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -126,7 +126,7 @@ "fieldname": "rate", "fieldtype": "Float", "in_list_view": 1, - "label": "Rate", + "label": "Tax Rate", "oldfieldname": "rate", "oldfieldtype": "Currency" }, @@ -239,4 +239,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 724c934fbbf246c181f40535f037827e39e60c74 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 14 Jan 2024 10:15:50 +0530 Subject: [PATCH 33/48] fix: modified date was not set (cherry picked from commit 566876ae7a9ac4ce70f0c72dd69c933ed022bf46) --- .../purchase_taxes_and_charges/purchase_taxes_and_charges.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 249e7518f0b..f10e9842a49 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -230,7 +230,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-05 20:04:36.618240", + "modified": "2021-08-06 20:04:36.618240", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", From e9d2437c7ac0a547191658d4694370af863cd450 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 14 Jan 2024 10:45:43 +0530 Subject: [PATCH 34/48] fix: modified date (cherry picked from commit 6827edb2c51ffe44f5af32fc63e065481bb85662) --- .../purchase_taxes_and_charges/purchase_taxes_and_charges.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index f10e9842a49..adab54b3756 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -230,7 +230,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-06 20:04:36.618240", + "modified": "2024-01-14 10:04:36.618240", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", From d0e3458c8c696fcf5be2b2cfbea47dfb1a260193 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 14 Jan 2024 15:52:37 +0530 Subject: [PATCH 35/48] fix: incorrect sql error if account name has '%' (cherry picked from commit 641c3de0caf3dd542a353edd78c8c18f686b8cae) --- .../report/customer_ledger_summary/customer_ledger_summary.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index b2222c2d6a7..2da6d18f005 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -376,6 +376,10 @@ class PartyLedgerSummaryReport(object): if not income_or_expense_accounts: # prevent empty 'in' condition income_or_expense_accounts.append("") + else: + # escape '%' in account name + # ignoring frappe.db.escape as it replaces single quotes with double quotes + income_or_expense_accounts = [x.replace("%", "%%") for x in income_or_expense_accounts] accounts_query = ( qb.from_(gl) From 4af3159f6245b414d498e8f40d75eaffbbdbdaaa Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 12:11:17 +0530 Subject: [PATCH 36/48] ci: bump node in release workflow (backport #39377) (#39379) * ci: bump node in release workflow (cherry picked from commit aef87cced7da0524c7ca2dadfcd111aaf13ef0c2) # Conflicts: # .github/workflows/release.yml * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ccd712065dc..e6a7f85f81b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: 18 + node-version: 20 - name: Setup dependencies run: | From a6bc5cae90465d6d517c59c61769623724f8eacd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 15 Jan 2024 14:10:55 +0530 Subject: [PATCH 37/48] fix: pass accounts as list to query --- erpnext/accounts/report/purchase_register/purchase_register.py | 2 +- erpnext/accounts/report/sales_register/sales_register.py | 2 +- erpnext/accounts/report/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index 4eb135b0487..b4ab5b8fa4e 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -433,7 +433,7 @@ def get_payments(filters): account_fieldname="paid_to", party="supplier", party_name="supplier_name", - party_account=get_party_account("Supplier", filters.supplier, filters.company), + party_account=[get_party_account("Supplier", filters.supplier, filters.company)], ) payment_entries = get_payment_entries(filters, args) journal_entries = get_journal_entries(filters, args) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 61b1fe2293c..1cb72f8d2d6 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -477,7 +477,7 @@ def get_payments(filters): account_fieldname="paid_from", party="customer", party_name="customer_name", - party_account=get_party_account("Customer", filters.customer, filters.company), + party_account=[get_party_account("Customer", filters.customer, filters.company)], ) payment_entries = get_payment_entries(filters, args) journal_entries = get_journal_entries(filters, args) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 01bc29fedf3..79ab799ccd3 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -253,7 +253,7 @@ def get_journal_entries(filters, args): (je.voucher_type == "Journal Entry") & (je.docstatus == 1) & (journal_account.party == filters.get(args.party)) - & (journal_account.account == args.party_account) + & (journal_account.account.isin(args.party_account)) ) .orderby(je.posting_date, je.name, order=Order.desc) ) From 4ea72f4b692a846bbd27f75054c229c3f7c167dd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 15 Jan 2024 20:22:30 +0530 Subject: [PATCH 38/48] fix: possible typerror in utils.js and remove unwanted debugging statements (cherry picked from commit 60b26ad8b262752e7d491b3fe21b398c0928bfaf) --- .../report/budget_variance_report/budget_variance_report.js | 4 ---- erpnext/public/js/utils.js | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index ca1cca13115..c9ddde9b5fb 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -84,10 +84,6 @@ function get_filters() { options: budget_against_options, default: "Cost Center", reqd: 1, - get_data: function() { - console.log(this.options); - return ["Emacs", "Rocks"]; - }, on_change: function() { frappe.query_report.set_filter_value("budget_against_filter", []); frappe.query_report.refresh(); diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 682762678ba..4c76e2a869e 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -21,7 +21,7 @@ $.extend(erpnext, { }, toggle_naming_series: function() { - if(cur_frm.fields_dict.naming_series) { + if(cur_frm && cur_frm.fields_dict.naming_series) { cur_frm.toggle_display("naming_series", cur_frm.doc.__islocal?true:false); } }, From 4b197920c1e3e816f76f153fc8d755820a63eaf6 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 16 Jan 2024 15:53:53 +0530 Subject: [PATCH 39/48] fix: show bill_date and bill_no in Purchase Register --- erpnext/accounts/report/purchase_register/purchase_register.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index b4ab5b8fa4e..f745c87a00a 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -89,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None): "payable_account": inv.credit_to, "mode_of_payment": inv.mode_of_payment, "project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project, + "bill_no": inv.bill_no, + "bill_date": inv.bill_date, "remarks": inv.remarks, "purchase_order": ", ".join(purchase_order), "purchase_receipt": ", ".join(purchase_receipt), From f2e577bec749fcbba74c224e2869433f24d7a4bb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 16 Jan 2024 13:38:53 +0530 Subject: [PATCH 40/48] fix: project query controller logic (cherry picked from commit 4eefb445a748100f3c36094188e38c127ad80051) # Conflicts: # erpnext/controllers/queries.py --- erpnext/controllers/queries.py | 59 +++++++++++++++++----------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index b73ebf53ae8..aa06dad4598 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -6,9 +6,15 @@ import json from collections import defaultdict import frappe -from frappe import scrub +from frappe import qb, scrub from frappe.desk.reportview import get_filters_cond, get_match_cond +<<<<<<< HEAD from frappe.utils import nowdate, unique +======= +from frappe.query_builder import Criterion +from frappe.query_builder.functions import Concat, Sum +from frappe.utils import nowdate, today, unique +>>>>>>> 4eefb445a7 (fix: project query controller logic) import erpnext from erpnext.stock.get_item_details import _get_item_tax_template @@ -329,37 +335,32 @@ def bom(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_project_name(doctype, txt, searchfield, start, page_len, filters): - doctype = "Project" - cond = "" + proj = qb.DocType("Project") + qb_filter_and_conditions = [] + qb_filter_or_conditions = [] if filters and filters.get("customer"): - cond = """(`tabProject`.customer = %s or - ifnull(`tabProject`.customer,"")="") and""" % ( - frappe.db.escape(filters.get("customer")) - ) + qb_filter_and_conditions.append(proj.customer == filters.get("customer")) - fields = get_fields(doctype, ["name", "project_name"]) - searchfields = frappe.get_meta(doctype).get_search_fields() - searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields]) + qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"])) - return frappe.db.sql( - """select {fields} from `tabProject` - where - `tabProject`.status not in ('Completed', 'Cancelled') - and {cond} {scond} {match_cond} - order by - (case when locate(%(_txt)s, `tabProject`.name) > 0 then locate(%(_txt)s, `tabProject`.name) else 99999 end), - `tabProject`.idx desc, - `tabProject`.name asc - limit {page_len} offset {start}""".format( - fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]), - cond=cond, - scond=searchfields, - match_cond=get_match_cond(doctype), - start=start, - page_len=page_len, - ), - {"txt": "%{0}%".format(txt), "_txt": txt.replace("%", "")}, - ) + q = qb.from_(proj) + + fields = get_fields("Project", ["name", "project_name"]) + for x in fields: + q = q.select(proj[x]) + + # ignore 'customer' and 'status' on searchfields as they must be exactly matched + searchfields = [ + x for x in frappe.get_meta(doctype).get_search_fields() if x not in ["customer", "status"] + ] + if txt: + for x in searchfields: + qb_filter_or_conditions.append(proj[x].like(f"%{txt}%")) + + q = q.where(Criterion.all(qb_filter_and_conditions)).where(Criterion.any(qb_filter_or_conditions)) + if page_len: + q = q.limit(page_len) + return q.run() @frappe.whitelist() From 98967ed58487cc63e0ed345c50b6f0fb1be97eea Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 16 Jan 2024 14:28:09 +0530 Subject: [PATCH 41/48] fix(test): test case for project query (cherry picked from commit 3349dde5e2914bd9e2dbe0ce4de94023bfee2e7f) --- erpnext/controllers/queries.py | 2 +- erpnext/controllers/tests/test_queries.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index aa06dad4598..c6285fc7344 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -345,7 +345,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): q = qb.from_(proj) - fields = get_fields("Project", ["name", "project_name"]) + fields = get_fields(doctype, ["name", "project_name"]) for x in fields: q = q.select(proj[x]) diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py index 60d1733021c..3a3bc1cd725 100644 --- a/erpnext/controllers/tests/test_queries.py +++ b/erpnext/controllers/tests/test_queries.py @@ -68,7 +68,7 @@ class TestQueries(unittest.TestCase): self.assertGreaterEqual(len(query(txt="_Test Item Home Desktop Manufactured")), 1) def test_project_query(self): - query = add_default_params(queries.get_project_name, "BOM") + query = add_default_params(queries.get_project_name, "Project") self.assertGreaterEqual(len(query(txt="_Test Project")), 1) From b35a83ee47f0d8d37112a458d79cfe6c2f452e39 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 16 Jan 2024 14:35:06 +0530 Subject: [PATCH 42/48] refactor: better ordering of query result (cherry picked from commit bfe42fdccb13ab797ac7252ada58df49af43ad54) # Conflicts: # erpnext/controllers/queries.py --- erpnext/controllers/queries.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index c6285fc7344..f581fb429bd 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -9,12 +9,19 @@ import frappe from frappe import qb, scrub from frappe.desk.reportview import get_filters_cond, get_match_cond <<<<<<< HEAD +<<<<<<< HEAD from frappe.utils import nowdate, unique ======= from frappe.query_builder import Criterion from frappe.query_builder.functions import Concat, Sum from frappe.utils import nowdate, today, unique >>>>>>> 4eefb445a7 (fix: project query controller logic) +======= +from frappe.query_builder import Criterion, CustomFunction +from frappe.query_builder.functions import Concat, Locate, Sum +from frappe.utils import nowdate, today, unique +from pypika import Order +>>>>>>> bfe42fdccb (refactor: better ordering of query result) import erpnext from erpnext.stock.get_item_details import _get_item_tax_template @@ -338,6 +345,8 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): proj = qb.DocType("Project") qb_filter_and_conditions = [] qb_filter_or_conditions = [] + ifelse = CustomFunction("IF", ["condition", "then", "else"]) + if filters and filters.get("customer"): qb_filter_and_conditions.append(proj.customer == filters.get("customer")) @@ -349,17 +358,29 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): for x in fields: q = q.select(proj[x]) - # ignore 'customer' and 'status' on searchfields as they must be exactly matched + # don't consider 'customer' and 'status' fields for pattern search, as they must be exactly matched searchfields = [ x for x in frappe.get_meta(doctype).get_search_fields() if x not in ["customer", "status"] ] + + # pattern search if txt: for x in searchfields: qb_filter_or_conditions.append(proj[x].like(f"%{txt}%")) q = q.where(Criterion.all(qb_filter_and_conditions)).where(Criterion.any(qb_filter_or_conditions)) + + # ordering + if txt: + # project_name containing search string 'txt' will be given higher precedence + q = q.orderby(ifelse(Locate(txt, proj.project_name) > 0, Locate(txt, proj.project_name), 99999)) + q = q.orderby(proj.idx, order=Order.desc).orderby(proj.name) + if page_len: q = q.limit(page_len) + + if start: + q = q.offset(start) return q.run() From 42c1de640ce5686b41eb5937e64d6f1351d808c8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 17 Jan 2024 10:36:29 +0530 Subject: [PATCH 43/48] chore: resolve conflict --- erpnext/controllers/queries.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index f581fb429bd..06ea8336bd6 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -8,20 +8,10 @@ from collections import defaultdict import frappe from frappe import qb, scrub from frappe.desk.reportview import get_filters_cond, get_match_cond -<<<<<<< HEAD -<<<<<<< HEAD -from frappe.utils import nowdate, unique -======= -from frappe.query_builder import Criterion -from frappe.query_builder.functions import Concat, Sum -from frappe.utils import nowdate, today, unique ->>>>>>> 4eefb445a7 (fix: project query controller logic) -======= from frappe.query_builder import Criterion, CustomFunction -from frappe.query_builder.functions import Concat, Locate, Sum -from frappe.utils import nowdate, today, unique +from frappe.query_builder.functions import Locate +from frappe.utils import nowdate, unique from pypika import Order ->>>>>>> bfe42fdccb (refactor: better ordering of query result) import erpnext from erpnext.stock.get_item_details import _get_item_tax_template From 3989b9757968e4cc5f9676dab5e034ea08b0191e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 15 Jan 2024 16:54:19 +0530 Subject: [PATCH 44/48] fix: WDV as per IT Act: calculate yearly amount first and then split it based on months (cherry picked from commit 22bd6a54b24129403e0b399938bddcaa9d630cae) # Conflicts: # erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py --- .../asset_depreciation_schedule.py | 1020 +++++++++++++++++ 1 file changed, 1020 insertions(+) create mode 100644 erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py new file mode 100644 index 00000000000..ffb50ebe452 --- /dev/null +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -0,0 +1,1020 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import ( + add_days, + add_months, + add_years, + cint, + date_diff, + flt, + get_first_day, + get_last_day, + getdate, + is_last_day_of_the_month, + month_diff, +) + +import erpnext +from erpnext.accounts.utils import get_fiscal_year + + +class AssetDepreciationSchedule(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.assets.doctype.depreciation_schedule.depreciation_schedule import ( + DepreciationSchedule, + ) + + amended_from: DF.Link | None + asset: DF.Link + company: DF.Link | None + daily_prorata_based: DF.Check + depreciation_method: DF.Literal[ + "", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual" + ] + depreciation_schedule: DF.Table[DepreciationSchedule] + expected_value_after_useful_life: DF.Currency + finance_book: DF.Link | None + finance_book_id: DF.Int + frequency_of_depreciation: DF.Int + gross_purchase_amount: DF.Currency + naming_series: DF.Literal["ACC-ADS-.YYYY.-"] + notes: DF.SmallText | None + number_of_depreciations_booked: DF.Int + opening_accumulated_depreciation: DF.Currency + rate_of_depreciation: DF.Percent + shift_based: DF.Check + status: DF.Literal["Draft", "Active", "Cancelled"] + total_number_of_depreciations: DF.Int + # end: auto-generated types + + def before_save(self): + if not self.finance_book_id: + self.prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name( + self.asset, self.finance_book + ) + self.update_shift_depr_schedule() + + def validate(self): + self.validate_another_asset_depr_schedule_does_not_exist() + + def validate_another_asset_depr_schedule_does_not_exist(self): + finance_book_filter = ["finance_book", "is", "not set"] + if self.finance_book: + finance_book_filter = ["finance_book", "=", self.finance_book] + + asset_depr_schedule = frappe.db.exists( + "Asset Depreciation Schedule", + [ + ["asset", "=", self.asset], + finance_book_filter, + ["docstatus", "<", 2], + ], + ) + + if asset_depr_schedule and asset_depr_schedule != self.name: + if self.finance_book: + frappe.throw( + _( + "Asset Depreciation Schedule {0} for Asset {1} and Finance Book {2} already exists." + ).format(asset_depr_schedule, self.asset, self.finance_book) + ) + else: + frappe.throw( + _("Asset Depreciation Schedule {0} for Asset {1} already exists.").format( + asset_depr_schedule, self.asset + ) + ) + + def on_submit(self): + self.db_set("status", "Active") + + def before_cancel(self): + if not self.flags.should_not_cancel_depreciation_entries: + self.cancel_depreciation_entries() + + def cancel_depreciation_entries(self): + for d in self.get("depreciation_schedule"): + if d.journal_entry: + frappe.get_doc("Journal Entry", d.journal_entry).cancel() + + def on_cancel(self): + self.db_set("status", "Cancelled") + + def update_shift_depr_schedule(self): + if not self.shift_based or self.docstatus != 0: + return + + asset_doc = frappe.get_doc("Asset", self.asset) + fb_row = asset_doc.finance_books[self.finance_book_id - 1] + + self.make_depr_schedule(asset_doc, fb_row) + self.set_accumulated_depreciation(asset_doc, fb_row) + + def prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(self, asset_name, fb_name): + asset_doc = frappe.get_doc("Asset", asset_name) + + finance_book_filter = ["finance_book", "is", "not set"] + if fb_name: + finance_book_filter = ["finance_book", "=", fb_name] + + asset_finance_book_name = frappe.db.get_value( + doctype="Asset Finance Book", + filters=[["parent", "=", asset_name], finance_book_filter], + ) + asset_finance_book_doc = frappe.get_doc("Asset Finance Book", asset_finance_book_name) + + self.prepare_draft_asset_depr_schedule_data(asset_doc, asset_finance_book_doc) + + def prepare_draft_asset_depr_schedule_data( + self, + asset_doc, + row, + date_of_disposal=None, + date_of_return=None, + update_asset_finance_book_row=True, + ): + have_asset_details_been_modified = self.have_asset_details_been_modified(asset_doc) + not_manual_depr_or_have_manual_depr_details_been_modified = ( + self.not_manual_depr_or_have_manual_depr_details_been_modified(row) + ) + + self.set_draft_asset_depr_schedule_details(asset_doc, row) + + if self.should_prepare_depreciation_schedule( + have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified + ): + self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row) + self.set_accumulated_depreciation(asset_doc, row, date_of_disposal, date_of_return) + + def have_asset_details_been_modified(self, asset_doc): + return ( + asset_doc.gross_purchase_amount != self.gross_purchase_amount + or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation + or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked + ) + + def not_manual_depr_or_have_manual_depr_details_been_modified(self, row): + return ( + self.depreciation_method != "Manual" + or row.total_number_of_depreciations != self.total_number_of_depreciations + or row.frequency_of_depreciation != self.frequency_of_depreciation + or getdate(row.depreciation_start_date) != self.get("depreciation_schedule")[0].schedule_date + or row.expected_value_after_useful_life != self.expected_value_after_useful_life + ) + + def should_prepare_depreciation_schedule( + self, have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified + ): + if not self.get("depreciation_schedule"): + return True + + old_asset_depr_schedule_doc = self.get_doc_before_save() + + if self.docstatus != 0 and not old_asset_depr_schedule_doc: + return True + + if have_asset_details_been_modified or not_manual_depr_or_have_manual_depr_details_been_modified: + return True + + return False + + def set_draft_asset_depr_schedule_details(self, asset_doc, row): + self.asset = asset_doc.name + self.finance_book = row.finance_book + self.finance_book_id = row.idx + self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0 + self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0 + self.gross_purchase_amount = asset_doc.gross_purchase_amount + self.depreciation_method = row.depreciation_method + self.total_number_of_depreciations = row.total_number_of_depreciations + self.frequency_of_depreciation = row.frequency_of_depreciation + self.rate_of_depreciation = row.rate_of_depreciation + self.expected_value_after_useful_life = row.expected_value_after_useful_life + self.daily_prorata_based = row.daily_prorata_based + self.shift_based = row.shift_based + self.status = "Draft" + + def make_depr_schedule( + self, + asset_doc, + row, + date_of_disposal=None, + update_asset_finance_book_row=True, + value_after_depreciation=None, + ): + if not self.get("depreciation_schedule"): + self.depreciation_schedule = [] + + if not asset_doc.available_for_use_date: + return + + start = self.clear_depr_schedule() + + self._make_depr_schedule( + asset_doc, row, start, date_of_disposal, update_asset_finance_book_row, value_after_depreciation + ) + + def clear_depr_schedule(self): + start = 0 + num_of_depreciations_completed = 0 + depr_schedule = [] + + self.schedules_before_clearing = self.get("depreciation_schedule") + + for schedule in self.get("depreciation_schedule"): + if schedule.journal_entry: + num_of_depreciations_completed += 1 + depr_schedule.append(schedule) + else: + start = num_of_depreciations_completed + break + + self.depreciation_schedule = depr_schedule + + return start + + def _make_depr_schedule( + self, + asset_doc, + row, + start, + date_of_disposal, + update_asset_finance_book_row, + value_after_depreciation, + ): + asset_doc.validate_asset_finance_books(row) + + if not value_after_depreciation: + 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: + row.db_update() + + final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint( + self.number_of_depreciations_booked + ) + + has_pro_rata = _check_is_pro_rata(asset_doc, row) + if has_pro_rata: + final_number_of_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 + + number_of_pending_depreciations = final_number_of_depreciations - start + yearly_opening_wdv = value_after_depreciation + current_fiscal_year_end_date = None + for n in range(start, final_number_of_depreciations): + # If depreciation is already completed (for double declining balance) + if skip_row: + continue + + schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation)) + if not current_fiscal_year_end_date: + current_fiscal_year_end_date = get_fiscal_year(row.depreciation_start_date)[2] + elif getdate(schedule_date) > getdate(current_fiscal_year_end_date): + current_fiscal_year_end_date = add_years(current_fiscal_year_end_date, 1) + yearly_opening_wdv = value_after_depreciation + + 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 + + depreciation_amount = get_depreciation_amount( + self, + asset_doc, + value_after_depreciation, + yearly_opening_wdv, + row, + n, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + number_of_pending_depreciations, + ) + + if not has_pro_rata or ( + n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2 + ): + schedule_date = add_months( + row.depreciation_start_date, n * cint(row.frequency_of_depreciation) + ) + + if should_get_last_day: + schedule_date = get_last_day(schedule_date) + + # if asset is being sold or scrapped + if date_of_disposal: + from_date = add_months( + getdate(asset_doc.available_for_use_date), + (asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation), + ) + if self.depreciation_schedule: + from_date = self.depreciation_schedule[-1].schedule_date + + depreciation_amount, days, months = _get_pro_rata_amt( + row, + depreciation_amount, + from_date, + date_of_disposal, + ) + + if depreciation_amount > 0: + self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n) + + break + + # For first row + if ( + n == 0 + and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) + and not self.opening_accumulated_depreciation + and not self.flags.wdv_it_act_applied + ): + 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 = _get_pro_rata_amt( + row, + depreciation_amount, + from_date, + row.depreciation_start_date, + has_wdv_or_dd_non_yearly_pro_rata, + ) + elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation: + if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)): + from_date = get_last_day( + add_months( + getdate(asset_doc.available_for_use_date), + ((self.number_of_depreciations_booked - 1) * row.frequency_of_depreciation), + ) + ) + else: + from_date = add_months( + getdate(add_days(asset_doc.available_for_use_date, -1)), + (self.number_of_depreciations_booked * row.frequency_of_depreciation), + ) + 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 last row + elif has_pro_rata and n == cint(final_number_of_depreciations) - 1: + if not asset_doc.flags.increase_in_asset_life: + # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission + asset_doc.to_date = add_months( + asset_doc.available_for_use_date, + (n + self.number_of_depreciations_booked) * cint(row.frequency_of_depreciation), + ) + + depreciation_amount_without_pro_rata = depreciation_amount + + 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( + depreciation_amount_without_pro_rata, depreciation_amount + ) + + schedule_date = add_days(schedule_date, days) + + if not depreciation_amount: + continue + value_after_depreciation = flt( + value_after_depreciation - flt(depreciation_amount), + asset_doc.precision("gross_purchase_amount"), + ) + + # Adjust depreciation amount in the last period based on the expected value after useful life + if row.expected_value_after_useful_life and ( + ( + n == cint(final_number_of_depreciations) - 1 + and value_after_depreciation != row.expected_value_after_useful_life + ) + or value_after_depreciation < row.expected_value_after_useful_life + ): + depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life + skip_row = True + + if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0: + self.add_depr_schedule_row(schedule_date, depreciation_amount, n) + + # to ensure that final accumulated depreciation amount is accurate + def get_adjusted_depreciation_amount( + self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row + ): + if not self.opening_accumulated_depreciation: + depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row() + + if ( + depreciation_amount_for_first_row + depreciation_amount_for_last_row + != depreciation_amount_without_pro_rata + ): + depreciation_amount_for_last_row = ( + depreciation_amount_without_pro_rata - depreciation_amount_for_first_row + ) + + return depreciation_amount_for_last_row + + def get_depreciation_amount_for_first_row(self): + return self.get("depreciation_schedule")[0].depreciation_amount + + def add_depr_schedule_row(self, schedule_date, depreciation_amount, schedule_idx): + if self.shift_based: + shift = ( + self.schedules_before_clearing[schedule_idx].shift + if self.schedules_before_clearing and len(self.schedules_before_clearing) > schedule_idx + else frappe.get_cached_value("Asset Shift Factor", {"default": 1}, "shift_name") + ) + else: + shift = None + + self.append( + "depreciation_schedule", + { + "schedule_date": schedule_date, + "depreciation_amount": depreciation_amount, + "shift": shift, + }, + ) + + def set_accumulated_depreciation( + self, + asset_doc, + row, + date_of_disposal=None, + date_of_return=None, + ignore_booked_entry=False, + ): + straight_line_idx = [ + d.idx + for d in self.get("depreciation_schedule") + if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual" + ] + + accumulated_depreciation = None + value_after_depreciation = flt(row.value_after_depreciation) + + for i, d in enumerate(self.get("depreciation_schedule")): + if ignore_booked_entry and d.journal_entry: + continue + + if not accumulated_depreciation: + if i > 0 and asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment: + accumulated_depreciation = self.get("depreciation_schedule")[ + i - 1 + ].accumulated_depreciation_amount + else: + accumulated_depreciation = flt(self.opening_accumulated_depreciation) + + depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) + value_after_depreciation -= flt(depreciation_amount) + + # for the last row, if depreciation method = Straight Line + if ( + straight_line_idx + and i == max(straight_line_idx) - 1 + and not date_of_disposal + and not date_of_return + and not row.shift_based + ): + depreciation_amount += flt( + value_after_depreciation - flt(row.expected_value_after_useful_life), + d.precision("depreciation_amount"), + ) + + d.depreciation_amount = depreciation_amount + accumulated_depreciation += d.depreciation_amount + d.accumulated_depreciation_amount = flt( + accumulated_depreciation, d.precision("accumulated_depreciation_amount") + ) + + +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) + + +def get_depreciation_amount( + asset_depr_schedule, + asset, + depreciable_value, + yearly_opening_wdv, + fb_row, + schedule_idx=0, + prev_depreciation_amount=0, + has_wdv_or_dd_non_yearly_pro_rata=False, + number_of_pending_depreciations=0, +): + if fb_row.depreciation_method in ("Straight Line", "Manual"): + return get_straight_line_or_manual_depr_amount( + asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations + ) + else: + return get_wdv_or_dd_depr_amount( + asset, + fb_row, + depreciable_value, + yearly_opening_wdv, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + asset_depr_schedule, + ) + + +def get_straight_line_or_manual_depr_amount( + asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations +): + if row.shift_based: + return get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx) + + # 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 modified after Asset Value Adjustment due to decrease in asset value + elif asset.flags.decrease_in_asset_value_due_to_value_adjustment: + if row.daily_prorata_based: + daily_depr_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / date_diff( + get_last_day( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) + * row.frequency_of_depreciation, + ) + ), + add_days( + get_last_day( + add_months( + row.depreciation_start_date, + flt( + row.total_number_of_depreciations + - asset.number_of_depreciations_booked + - number_of_pending_depreciations + - 1 + ) + * row.frequency_of_depreciation, + ) + ), + 1, + ), + ) + + to_date = get_last_day( + add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) + ) + from_date = add_days( + get_last_day( + add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation) + ), + 1, + ) + + return daily_depr_amount * (date_diff(to_date, from_date) + 1) + else: + return ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / number_of_pending_depreciations + # if the Depreciation Schedule is being prepared for the first time + else: + if row.daily_prorata_based: + daily_depr_amount = ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) / date_diff( + get_last_day( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) + * row.frequency_of_depreciation, + ) + ), + add_days( + get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)), 1 + ), + ) + + to_date = get_last_day( + add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) + ) + from_date = add_days( + get_last_day( + add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation) + ), + 1, + ) + + return daily_depr_amount * (date_diff(to_date, from_date) + 1) + else: + return ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) + + +def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx): + if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation: + return ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) + + asset_shift_factors_map = get_asset_shift_factors_map() + shift = ( + asset_depr_schedule.schedules_before_clearing[schedule_idx].shift + if len(asset_depr_schedule.schedules_before_clearing) > schedule_idx + else None + ) + shift_factor = asset_shift_factors_map.get(shift) if shift else 0 + + shift_factors_sum = sum( + flt(asset_shift_factors_map.get(schedule.shift)) + for schedule in asset_depr_schedule.schedules_before_clearing + ) + + return ( + ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) + / flt(shift_factors_sum) + ) * shift_factor + + +def get_asset_shift_factors_map(): + return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True)) + + +@erpnext.allow_regional +def get_wdv_or_dd_depr_amount( + asset, + fb_row, + depreciable_value, + yearly_opening_wdv, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + asset_depr_schedule, +): + return ( + get_default_wdv_or_dd_depr_amount( + asset, + fb_row, + depreciable_value, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + asset_depr_schedule, + ), + None, + ) + + +def get_default_wdv_or_dd_depr_amount( + asset, + fb_row, + depreciable_value, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + asset_depr_schedule, +): + if cint(fb_row.frequency_of_depreciation) == 12: + return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100) + else: + if has_wdv_or_dd_non_yearly_pro_rata: + if schedule_idx == 0: + return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100) + elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: + return ( + flt(depreciable_value) + * flt(fb_row.frequency_of_depreciation) + * (flt(fb_row.rate_of_depreciation) / 1200) + ) + else: + return prev_depreciation_amount + else: + if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: + return ( + flt(depreciable_value) + * flt(fb_row.frequency_of_depreciation) + * (flt(fb_row.rate_of_depreciation) / 1200) + ) + else: + return prev_depreciation_amount + + +def make_draft_asset_depr_schedules_if_not_present(asset_doc): + asset_depr_schedules_names = [] + + for row in asset_doc.get("finance_books"): + draft_asset_depr_schedule_name = get_asset_depr_schedule_name( + asset_doc.name, "Draft", row.finance_book + ) + + active_asset_depr_schedule_name = get_asset_depr_schedule_name( + asset_doc.name, "Active", row.finance_book + ) + + if not draft_asset_depr_schedule_name and not active_asset_depr_schedule_name: + name = make_draft_asset_depr_schedule(asset_doc, row) + asset_depr_schedules_names.append(name) + + return asset_depr_schedules_names + + +def make_draft_asset_depr_schedules(asset_doc): + asset_depr_schedules_names = [] + + for row in asset_doc.get("finance_books"): + name = make_draft_asset_depr_schedule(asset_doc, row) + asset_depr_schedules_names.append(name) + + return asset_depr_schedules_names + + +def make_draft_asset_depr_schedule(asset_doc, row): + asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + + asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row) + + asset_depr_schedule_doc.insert() + + return asset_depr_schedule_doc.name + + +def update_draft_asset_depr_schedules(asset_doc): + for row in asset_doc.get("finance_books"): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book) + + if not asset_depr_schedule_doc: + continue + + asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row) + + asset_depr_schedule_doc.save() + + +def convert_draft_asset_depr_schedules_into_active(asset_doc): + for row in asset_doc.get("finance_books"): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book) + + if not asset_depr_schedule_doc: + continue + + asset_depr_schedule_doc.submit() + + +def cancel_asset_depr_schedules(asset_doc): + for row in asset_doc.get("finance_books"): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Active", row.finance_book) + + if not asset_depr_schedule_doc: + continue + + asset_depr_schedule_doc.cancel() + + +def make_new_active_asset_depr_schedules_and_cancel_current_ones( + asset_doc, + notes, + date_of_disposal=None, + date_of_return=None, + value_after_depreciation=None, + ignore_booked_entry=False, +): + for row in asset_doc.get("finance_books"): + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset_doc.name, "Active", row.finance_book + ) + + if not current_asset_depr_schedule_doc: + frappe.throw( + _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( + asset_doc.name, row.finance_book + ) + ) + + new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + + if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in ( + "Written Down Value", + "Double Declining Balance", + ): + new_rate_of_depreciation = flt( + asset_doc.get_depreciation_rate(row), row.precision("rate_of_depreciation") + ) + row.rate_of_depreciation = new_rate_of_depreciation + new_asset_depr_schedule_doc.rate_of_depreciation = new_rate_of_depreciation + + new_asset_depr_schedule_doc.make_depr_schedule( + asset_doc, row, date_of_disposal, value_after_depreciation=value_after_depreciation + ) + new_asset_depr_schedule_doc.set_accumulated_depreciation( + asset_doc, row, date_of_disposal, date_of_return, ignore_booked_entry + ) + + new_asset_depr_schedule_doc.notes = notes + + current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True + current_asset_depr_schedule_doc.cancel() + + new_asset_depr_schedule_doc.submit() + + +def get_temp_asset_depr_schedule_doc( + asset_doc, + row, + date_of_disposal=None, + date_of_return=None, + update_asset_finance_book_row=False, + new_depr_schedule=None, +): + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset_doc.name, "Active", row.finance_book + ) + + if not current_asset_depr_schedule_doc: + frappe.throw( + _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( + asset_doc.name, row.finance_book + ) + ) + + temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + + if new_depr_schedule: + temp_asset_depr_schedule_doc.depreciation_schedule = [] + + for schedule in new_depr_schedule: + temp_asset_depr_schedule_doc.append( + "depreciation_schedule", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + }, + ) + + temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data( + asset_doc, + row, + date_of_disposal, + date_of_return, + update_asset_finance_book_row, + ) + + return temp_asset_depr_schedule_doc + + +@frappe.whitelist() +def get_depr_schedule(asset_name, status, finance_book=None): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book) + + if not asset_depr_schedule_doc: + return + + return asset_depr_schedule_doc.get("depreciation_schedule") + + +@frappe.whitelist() +def get_asset_depr_schedule_doc(asset_name, status, finance_book=None): + asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book) + + if not asset_depr_schedule_name: + return + + asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name) + + return asset_depr_schedule_doc + + +def get_asset_depr_schedule_name(asset_name, status, finance_book=None): + finance_book_filter = ["finance_book", "is", "not set"] + if finance_book: + finance_book_filter = ["finance_book", "=", finance_book] + + return frappe.db.get_value( + doctype="Asset Depreciation Schedule", + filters=[ + ["asset", "=", asset_name], + finance_book_filter, + ["status", "=", status], + ], + ) + + +def is_first_day_of_the_month(date): + first_day_of_the_month = get_first_day(date) + + return getdate(first_day_of_the_month) == getdate(date) From 1dff96057c72991bfc579184d09edc58739fe01a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 15 Jan 2024 17:54:47 +0530 Subject: [PATCH 45/48] fix: Cancel asset capitalisation record on cancellation of asset and vice-versa (cherry picked from commit efe9f6656f01c46a6ac02e3bb61851564670d6bc) # Conflicts: # erpnext/assets/doctype/asset_capitalization/asset_capitalization.py --- erpnext/assets/doctype/asset/asset.json | 5 ++--- erpnext/assets/doctype/asset/asset.py | 11 +++++++++++ .../asset_capitalization.py | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index cdccf81507b..d8b8bf18a15 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -202,8 +202,7 @@ "fieldname": "purchase_date", "fieldtype": "Date", "label": "Purchase Date", - "mandatory_depends_on": "eval:!doc.is_existing_asset", - "read_only": 1, + "mandatory_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" }, { @@ -583,7 +582,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2024-01-05 17:36:53.131512", + "modified": "2024-01-15 17:35:49.226603", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 3db4a8d18bd..dc1ab378eee 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -61,6 +61,7 @@ class Asset(AccountsController): def on_cancel(self): self.validate_cancellation() self.cancel_movement_entries() + self.cancel_capitalization() self.delete_depreciation_entries() self.set_status() self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") @@ -831,6 +832,16 @@ class Asset(AccountsController): movement = frappe.get_doc("Asset Movement", movement.get("name")) movement.cancel() + def cancel_capitalization(self): + asset_capitalization = frappe.db.get_value( + "Asset Capitalization", + {"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"}, + ) + + if asset_capitalization: + asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization) + asset_capitalization.cancel() + def delete_depreciation_entries(self): if self.calculate_depreciation: for d in self.get("schedules"): diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 92cb85d1b7c..38c8de2beea 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -72,11 +72,28 @@ class AssetCapitalization(StockController): self.update_target_asset() def on_cancel(self): +<<<<<<< HEAD self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") +======= + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Repost Item Valuation", + "Serial and Batch Bundle", + "Asset", + ) + self.cancel_target_asset() +>>>>>>> efe9f6656f (fix: Cancel asset capitalisation record on cancellation of asset and vice-versa) self.update_stock_ledger() self.make_gl_entries() self.restore_consumed_asset_items() + def cancel_target_asset(self): + if self.entry_type == "Capitalization" and self.target_asset: + asset_doc = frappe.get_doc("Asset", self.target_asset) + if asset_doc.docstatus == 1: + asset_doc.cancel() + def set_title(self): self.title = self.target_asset_name or self.target_item_name or self.target_item_code From 8014839795d0e28ec53134f0225df97fed635985 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 17 Jan 2024 17:32:14 +0530 Subject: [PATCH 46/48] Revert "fix: WDV as per IT Act: calculate yearly amount first and then split it based on months (backport #39385)" --- .../asset_depreciation_schedule.py | 1020 ----------------- 1 file changed, 1020 deletions(-) delete mode 100644 erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py deleted file mode 100644 index ffb50ebe452..00000000000 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ /dev/null @@ -1,1020 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.utils import ( - add_days, - add_months, - add_years, - cint, - date_diff, - flt, - get_first_day, - get_last_day, - getdate, - is_last_day_of_the_month, - month_diff, -) - -import erpnext -from erpnext.accounts.utils import get_fiscal_year - - -class AssetDepreciationSchedule(Document): - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.assets.doctype.depreciation_schedule.depreciation_schedule import ( - DepreciationSchedule, - ) - - amended_from: DF.Link | None - asset: DF.Link - company: DF.Link | None - daily_prorata_based: DF.Check - depreciation_method: DF.Literal[ - "", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual" - ] - depreciation_schedule: DF.Table[DepreciationSchedule] - expected_value_after_useful_life: DF.Currency - finance_book: DF.Link | None - finance_book_id: DF.Int - frequency_of_depreciation: DF.Int - gross_purchase_amount: DF.Currency - naming_series: DF.Literal["ACC-ADS-.YYYY.-"] - notes: DF.SmallText | None - number_of_depreciations_booked: DF.Int - opening_accumulated_depreciation: DF.Currency - rate_of_depreciation: DF.Percent - shift_based: DF.Check - status: DF.Literal["Draft", "Active", "Cancelled"] - total_number_of_depreciations: DF.Int - # end: auto-generated types - - def before_save(self): - if not self.finance_book_id: - self.prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name( - self.asset, self.finance_book - ) - self.update_shift_depr_schedule() - - def validate(self): - self.validate_another_asset_depr_schedule_does_not_exist() - - def validate_another_asset_depr_schedule_does_not_exist(self): - finance_book_filter = ["finance_book", "is", "not set"] - if self.finance_book: - finance_book_filter = ["finance_book", "=", self.finance_book] - - asset_depr_schedule = frappe.db.exists( - "Asset Depreciation Schedule", - [ - ["asset", "=", self.asset], - finance_book_filter, - ["docstatus", "<", 2], - ], - ) - - if asset_depr_schedule and asset_depr_schedule != self.name: - if self.finance_book: - frappe.throw( - _( - "Asset Depreciation Schedule {0} for Asset {1} and Finance Book {2} already exists." - ).format(asset_depr_schedule, self.asset, self.finance_book) - ) - else: - frappe.throw( - _("Asset Depreciation Schedule {0} for Asset {1} already exists.").format( - asset_depr_schedule, self.asset - ) - ) - - def on_submit(self): - self.db_set("status", "Active") - - def before_cancel(self): - if not self.flags.should_not_cancel_depreciation_entries: - self.cancel_depreciation_entries() - - def cancel_depreciation_entries(self): - for d in self.get("depreciation_schedule"): - if d.journal_entry: - frappe.get_doc("Journal Entry", d.journal_entry).cancel() - - def on_cancel(self): - self.db_set("status", "Cancelled") - - def update_shift_depr_schedule(self): - if not self.shift_based or self.docstatus != 0: - return - - asset_doc = frappe.get_doc("Asset", self.asset) - fb_row = asset_doc.finance_books[self.finance_book_id - 1] - - self.make_depr_schedule(asset_doc, fb_row) - self.set_accumulated_depreciation(asset_doc, fb_row) - - def prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(self, asset_name, fb_name): - asset_doc = frappe.get_doc("Asset", asset_name) - - finance_book_filter = ["finance_book", "is", "not set"] - if fb_name: - finance_book_filter = ["finance_book", "=", fb_name] - - asset_finance_book_name = frappe.db.get_value( - doctype="Asset Finance Book", - filters=[["parent", "=", asset_name], finance_book_filter], - ) - asset_finance_book_doc = frappe.get_doc("Asset Finance Book", asset_finance_book_name) - - self.prepare_draft_asset_depr_schedule_data(asset_doc, asset_finance_book_doc) - - def prepare_draft_asset_depr_schedule_data( - self, - asset_doc, - row, - date_of_disposal=None, - date_of_return=None, - update_asset_finance_book_row=True, - ): - have_asset_details_been_modified = self.have_asset_details_been_modified(asset_doc) - not_manual_depr_or_have_manual_depr_details_been_modified = ( - self.not_manual_depr_or_have_manual_depr_details_been_modified(row) - ) - - self.set_draft_asset_depr_schedule_details(asset_doc, row) - - if self.should_prepare_depreciation_schedule( - have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified - ): - self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row) - self.set_accumulated_depreciation(asset_doc, row, date_of_disposal, date_of_return) - - def have_asset_details_been_modified(self, asset_doc): - return ( - asset_doc.gross_purchase_amount != self.gross_purchase_amount - or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation - or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked - ) - - def not_manual_depr_or_have_manual_depr_details_been_modified(self, row): - return ( - self.depreciation_method != "Manual" - or row.total_number_of_depreciations != self.total_number_of_depreciations - or row.frequency_of_depreciation != self.frequency_of_depreciation - or getdate(row.depreciation_start_date) != self.get("depreciation_schedule")[0].schedule_date - or row.expected_value_after_useful_life != self.expected_value_after_useful_life - ) - - def should_prepare_depreciation_schedule( - self, have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified - ): - if not self.get("depreciation_schedule"): - return True - - old_asset_depr_schedule_doc = self.get_doc_before_save() - - if self.docstatus != 0 and not old_asset_depr_schedule_doc: - return True - - if have_asset_details_been_modified or not_manual_depr_or_have_manual_depr_details_been_modified: - return True - - return False - - def set_draft_asset_depr_schedule_details(self, asset_doc, row): - self.asset = asset_doc.name - self.finance_book = row.finance_book - self.finance_book_id = row.idx - self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0 - self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0 - self.gross_purchase_amount = asset_doc.gross_purchase_amount - self.depreciation_method = row.depreciation_method - self.total_number_of_depreciations = row.total_number_of_depreciations - self.frequency_of_depreciation = row.frequency_of_depreciation - self.rate_of_depreciation = row.rate_of_depreciation - self.expected_value_after_useful_life = row.expected_value_after_useful_life - self.daily_prorata_based = row.daily_prorata_based - self.shift_based = row.shift_based - self.status = "Draft" - - def make_depr_schedule( - self, - asset_doc, - row, - date_of_disposal=None, - update_asset_finance_book_row=True, - value_after_depreciation=None, - ): - if not self.get("depreciation_schedule"): - self.depreciation_schedule = [] - - if not asset_doc.available_for_use_date: - return - - start = self.clear_depr_schedule() - - self._make_depr_schedule( - asset_doc, row, start, date_of_disposal, update_asset_finance_book_row, value_after_depreciation - ) - - def clear_depr_schedule(self): - start = 0 - num_of_depreciations_completed = 0 - depr_schedule = [] - - self.schedules_before_clearing = self.get("depreciation_schedule") - - for schedule in self.get("depreciation_schedule"): - if schedule.journal_entry: - num_of_depreciations_completed += 1 - depr_schedule.append(schedule) - else: - start = num_of_depreciations_completed - break - - self.depreciation_schedule = depr_schedule - - return start - - def _make_depr_schedule( - self, - asset_doc, - row, - start, - date_of_disposal, - update_asset_finance_book_row, - value_after_depreciation, - ): - asset_doc.validate_asset_finance_books(row) - - if not value_after_depreciation: - 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: - row.db_update() - - final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint( - self.number_of_depreciations_booked - ) - - has_pro_rata = _check_is_pro_rata(asset_doc, row) - if has_pro_rata: - final_number_of_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 - - number_of_pending_depreciations = final_number_of_depreciations - start - yearly_opening_wdv = value_after_depreciation - current_fiscal_year_end_date = None - for n in range(start, final_number_of_depreciations): - # If depreciation is already completed (for double declining balance) - if skip_row: - continue - - schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation)) - if not current_fiscal_year_end_date: - current_fiscal_year_end_date = get_fiscal_year(row.depreciation_start_date)[2] - elif getdate(schedule_date) > getdate(current_fiscal_year_end_date): - current_fiscal_year_end_date = add_years(current_fiscal_year_end_date, 1) - yearly_opening_wdv = value_after_depreciation - - 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 - - depreciation_amount = get_depreciation_amount( - self, - asset_doc, - value_after_depreciation, - yearly_opening_wdv, - row, - n, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - number_of_pending_depreciations, - ) - - if not has_pro_rata or ( - n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2 - ): - schedule_date = add_months( - row.depreciation_start_date, n * cint(row.frequency_of_depreciation) - ) - - if should_get_last_day: - schedule_date = get_last_day(schedule_date) - - # if asset is being sold or scrapped - if date_of_disposal: - from_date = add_months( - getdate(asset_doc.available_for_use_date), - (asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation), - ) - if self.depreciation_schedule: - from_date = self.depreciation_schedule[-1].schedule_date - - depreciation_amount, days, months = _get_pro_rata_amt( - row, - depreciation_amount, - from_date, - date_of_disposal, - ) - - if depreciation_amount > 0: - self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n) - - break - - # For first row - if ( - n == 0 - and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) - and not self.opening_accumulated_depreciation - and not self.flags.wdv_it_act_applied - ): - 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 = _get_pro_rata_amt( - row, - depreciation_amount, - from_date, - row.depreciation_start_date, - has_wdv_or_dd_non_yearly_pro_rata, - ) - elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation: - if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)): - from_date = get_last_day( - add_months( - getdate(asset_doc.available_for_use_date), - ((self.number_of_depreciations_booked - 1) * row.frequency_of_depreciation), - ) - ) - else: - from_date = add_months( - getdate(add_days(asset_doc.available_for_use_date, -1)), - (self.number_of_depreciations_booked * row.frequency_of_depreciation), - ) - 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 last row - elif has_pro_rata and n == cint(final_number_of_depreciations) - 1: - if not asset_doc.flags.increase_in_asset_life: - # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission - asset_doc.to_date = add_months( - asset_doc.available_for_use_date, - (n + self.number_of_depreciations_booked) * cint(row.frequency_of_depreciation), - ) - - depreciation_amount_without_pro_rata = depreciation_amount - - 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( - depreciation_amount_without_pro_rata, depreciation_amount - ) - - schedule_date = add_days(schedule_date, days) - - if not depreciation_amount: - continue - value_after_depreciation = flt( - value_after_depreciation - flt(depreciation_amount), - asset_doc.precision("gross_purchase_amount"), - ) - - # Adjust depreciation amount in the last period based on the expected value after useful life - if row.expected_value_after_useful_life and ( - ( - n == cint(final_number_of_depreciations) - 1 - and value_after_depreciation != row.expected_value_after_useful_life - ) - or value_after_depreciation < row.expected_value_after_useful_life - ): - depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life - skip_row = True - - if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0: - self.add_depr_schedule_row(schedule_date, depreciation_amount, n) - - # to ensure that final accumulated depreciation amount is accurate - def get_adjusted_depreciation_amount( - self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row - ): - if not self.opening_accumulated_depreciation: - depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row() - - if ( - depreciation_amount_for_first_row + depreciation_amount_for_last_row - != depreciation_amount_without_pro_rata - ): - depreciation_amount_for_last_row = ( - depreciation_amount_without_pro_rata - depreciation_amount_for_first_row - ) - - return depreciation_amount_for_last_row - - def get_depreciation_amount_for_first_row(self): - return self.get("depreciation_schedule")[0].depreciation_amount - - def add_depr_schedule_row(self, schedule_date, depreciation_amount, schedule_idx): - if self.shift_based: - shift = ( - self.schedules_before_clearing[schedule_idx].shift - if self.schedules_before_clearing and len(self.schedules_before_clearing) > schedule_idx - else frappe.get_cached_value("Asset Shift Factor", {"default": 1}, "shift_name") - ) - else: - shift = None - - self.append( - "depreciation_schedule", - { - "schedule_date": schedule_date, - "depreciation_amount": depreciation_amount, - "shift": shift, - }, - ) - - def set_accumulated_depreciation( - self, - asset_doc, - row, - date_of_disposal=None, - date_of_return=None, - ignore_booked_entry=False, - ): - straight_line_idx = [ - d.idx - for d in self.get("depreciation_schedule") - if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual" - ] - - accumulated_depreciation = None - value_after_depreciation = flt(row.value_after_depreciation) - - for i, d in enumerate(self.get("depreciation_schedule")): - if ignore_booked_entry and d.journal_entry: - continue - - if not accumulated_depreciation: - if i > 0 and asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment: - accumulated_depreciation = self.get("depreciation_schedule")[ - i - 1 - ].accumulated_depreciation_amount - else: - accumulated_depreciation = flt(self.opening_accumulated_depreciation) - - depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) - value_after_depreciation -= flt(depreciation_amount) - - # for the last row, if depreciation method = Straight Line - if ( - straight_line_idx - and i == max(straight_line_idx) - 1 - and not date_of_disposal - and not date_of_return - and not row.shift_based - ): - depreciation_amount += flt( - value_after_depreciation - flt(row.expected_value_after_useful_life), - d.precision("depreciation_amount"), - ) - - d.depreciation_amount = depreciation_amount - accumulated_depreciation += d.depreciation_amount - d.accumulated_depreciation_amount = flt( - accumulated_depreciation, d.precision("accumulated_depreciation_amount") - ) - - -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) - - -def get_depreciation_amount( - asset_depr_schedule, - asset, - depreciable_value, - yearly_opening_wdv, - fb_row, - schedule_idx=0, - prev_depreciation_amount=0, - has_wdv_or_dd_non_yearly_pro_rata=False, - number_of_pending_depreciations=0, -): - if fb_row.depreciation_method in ("Straight Line", "Manual"): - return get_straight_line_or_manual_depr_amount( - asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations - ) - else: - return get_wdv_or_dd_depr_amount( - asset, - fb_row, - depreciable_value, - yearly_opening_wdv, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, - ) - - -def get_straight_line_or_manual_depr_amount( - asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations -): - if row.shift_based: - return get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx) - - # 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 modified after Asset Value Adjustment due to decrease in asset value - elif asset.flags.decrease_in_asset_value_due_to_value_adjustment: - if row.daily_prorata_based: - daily_depr_amount = ( - flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) - ) / date_diff( - get_last_day( - add_months( - row.depreciation_start_date, - flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) - * row.frequency_of_depreciation, - ) - ), - add_days( - get_last_day( - add_months( - row.depreciation_start_date, - flt( - row.total_number_of_depreciations - - asset.number_of_depreciations_booked - - number_of_pending_depreciations - - 1 - ) - * row.frequency_of_depreciation, - ) - ), - 1, - ), - ) - - to_date = get_last_day( - add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) - ) - from_date = add_days( - get_last_day( - add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation) - ), - 1, - ) - - return daily_depr_amount * (date_diff(to_date, from_date) + 1) - else: - return ( - flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) - ) / number_of_pending_depreciations - # if the Depreciation Schedule is being prepared for the first time - else: - if row.daily_prorata_based: - daily_depr_amount = ( - flt(asset.gross_purchase_amount) - - flt(asset.opening_accumulated_depreciation) - - flt(row.expected_value_after_useful_life) - ) / date_diff( - get_last_day( - add_months( - row.depreciation_start_date, - flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) - * row.frequency_of_depreciation, - ) - ), - add_days( - get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)), 1 - ), - ) - - to_date = get_last_day( - add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) - ) - from_date = add_days( - get_last_day( - add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation) - ), - 1, - ) - - return daily_depr_amount * (date_diff(to_date, from_date) + 1) - else: - return ( - flt(asset.gross_purchase_amount) - - flt(asset.opening_accumulated_depreciation) - - flt(row.expected_value_after_useful_life) - ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) - - -def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx): - if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation: - return ( - flt(asset.gross_purchase_amount) - - flt(asset.opening_accumulated_depreciation) - - flt(row.expected_value_after_useful_life) - ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) - - asset_shift_factors_map = get_asset_shift_factors_map() - shift = ( - asset_depr_schedule.schedules_before_clearing[schedule_idx].shift - if len(asset_depr_schedule.schedules_before_clearing) > schedule_idx - else None - ) - shift_factor = asset_shift_factors_map.get(shift) if shift else 0 - - shift_factors_sum = sum( - flt(asset_shift_factors_map.get(schedule.shift)) - for schedule in asset_depr_schedule.schedules_before_clearing - ) - - return ( - ( - flt(asset.gross_purchase_amount) - - flt(asset.opening_accumulated_depreciation) - - flt(row.expected_value_after_useful_life) - ) - / flt(shift_factors_sum) - ) * shift_factor - - -def get_asset_shift_factors_map(): - return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True)) - - -@erpnext.allow_regional -def get_wdv_or_dd_depr_amount( - asset, - fb_row, - depreciable_value, - yearly_opening_wdv, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, -): - return ( - get_default_wdv_or_dd_depr_amount( - asset, - fb_row, - depreciable_value, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, - ), - None, - ) - - -def get_default_wdv_or_dd_depr_amount( - asset, - fb_row, - depreciable_value, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, -): - if cint(fb_row.frequency_of_depreciation) == 12: - return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100) - else: - if has_wdv_or_dd_non_yearly_pro_rata: - if schedule_idx == 0: - return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100) - elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: - return ( - flt(depreciable_value) - * flt(fb_row.frequency_of_depreciation) - * (flt(fb_row.rate_of_depreciation) / 1200) - ) - else: - return prev_depreciation_amount - else: - if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: - return ( - flt(depreciable_value) - * flt(fb_row.frequency_of_depreciation) - * (flt(fb_row.rate_of_depreciation) / 1200) - ) - else: - return prev_depreciation_amount - - -def make_draft_asset_depr_schedules_if_not_present(asset_doc): - asset_depr_schedules_names = [] - - for row in asset_doc.get("finance_books"): - draft_asset_depr_schedule_name = get_asset_depr_schedule_name( - asset_doc.name, "Draft", row.finance_book - ) - - active_asset_depr_schedule_name = get_asset_depr_schedule_name( - asset_doc.name, "Active", row.finance_book - ) - - if not draft_asset_depr_schedule_name and not active_asset_depr_schedule_name: - name = make_draft_asset_depr_schedule(asset_doc, row) - asset_depr_schedules_names.append(name) - - return asset_depr_schedules_names - - -def make_draft_asset_depr_schedules(asset_doc): - asset_depr_schedules_names = [] - - for row in asset_doc.get("finance_books"): - name = make_draft_asset_depr_schedule(asset_doc, row) - asset_depr_schedules_names.append(name) - - return asset_depr_schedules_names - - -def make_draft_asset_depr_schedule(asset_doc, row): - asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") - - asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row) - - asset_depr_schedule_doc.insert() - - return asset_depr_schedule_doc.name - - -def update_draft_asset_depr_schedules(asset_doc): - for row in asset_doc.get("finance_books"): - asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book) - - if not asset_depr_schedule_doc: - continue - - asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row) - - asset_depr_schedule_doc.save() - - -def convert_draft_asset_depr_schedules_into_active(asset_doc): - for row in asset_doc.get("finance_books"): - asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book) - - if not asset_depr_schedule_doc: - continue - - asset_depr_schedule_doc.submit() - - -def cancel_asset_depr_schedules(asset_doc): - for row in asset_doc.get("finance_books"): - asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Active", row.finance_book) - - if not asset_depr_schedule_doc: - continue - - asset_depr_schedule_doc.cancel() - - -def make_new_active_asset_depr_schedules_and_cancel_current_ones( - asset_doc, - notes, - date_of_disposal=None, - date_of_return=None, - value_after_depreciation=None, - ignore_booked_entry=False, -): - for row in asset_doc.get("finance_books"): - current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( - asset_doc.name, "Active", row.finance_book - ) - - if not current_asset_depr_schedule_doc: - frappe.throw( - _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( - asset_doc.name, row.finance_book - ) - ) - - new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) - - if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in ( - "Written Down Value", - "Double Declining Balance", - ): - new_rate_of_depreciation = flt( - asset_doc.get_depreciation_rate(row), row.precision("rate_of_depreciation") - ) - row.rate_of_depreciation = new_rate_of_depreciation - new_asset_depr_schedule_doc.rate_of_depreciation = new_rate_of_depreciation - - new_asset_depr_schedule_doc.make_depr_schedule( - asset_doc, row, date_of_disposal, value_after_depreciation=value_after_depreciation - ) - new_asset_depr_schedule_doc.set_accumulated_depreciation( - asset_doc, row, date_of_disposal, date_of_return, ignore_booked_entry - ) - - new_asset_depr_schedule_doc.notes = notes - - current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True - current_asset_depr_schedule_doc.cancel() - - new_asset_depr_schedule_doc.submit() - - -def get_temp_asset_depr_schedule_doc( - asset_doc, - row, - date_of_disposal=None, - date_of_return=None, - update_asset_finance_book_row=False, - new_depr_schedule=None, -): - current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( - asset_doc.name, "Active", row.finance_book - ) - - if not current_asset_depr_schedule_doc: - frappe.throw( - _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( - asset_doc.name, row.finance_book - ) - ) - - temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) - - if new_depr_schedule: - temp_asset_depr_schedule_doc.depreciation_schedule = [] - - for schedule in new_depr_schedule: - temp_asset_depr_schedule_doc.append( - "depreciation_schedule", - { - "schedule_date": schedule.schedule_date, - "depreciation_amount": schedule.depreciation_amount, - "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, - "journal_entry": schedule.journal_entry, - "shift": schedule.shift, - }, - ) - - temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data( - asset_doc, - row, - date_of_disposal, - date_of_return, - update_asset_finance_book_row, - ) - - return temp_asset_depr_schedule_doc - - -@frappe.whitelist() -def get_depr_schedule(asset_name, status, finance_book=None): - asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book) - - if not asset_depr_schedule_doc: - return - - return asset_depr_schedule_doc.get("depreciation_schedule") - - -@frappe.whitelist() -def get_asset_depr_schedule_doc(asset_name, status, finance_book=None): - asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book) - - if not asset_depr_schedule_name: - return - - asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name) - - return asset_depr_schedule_doc - - -def get_asset_depr_schedule_name(asset_name, status, finance_book=None): - finance_book_filter = ["finance_book", "is", "not set"] - if finance_book: - finance_book_filter = ["finance_book", "=", finance_book] - - return frappe.db.get_value( - doctype="Asset Depreciation Schedule", - filters=[ - ["asset", "=", asset_name], - finance_book_filter, - ["status", "=", status], - ], - ) - - -def is_first_day_of_the_month(date): - first_day_of_the_month = get_first_day(date) - - return getdate(first_day_of_the_month) == getdate(date) From 4edb73d398d921d76e860cb072b00af20c272cca Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 17 Jan 2024 17:36:42 +0530 Subject: [PATCH 47/48] fix: resolved merge conflict --- .../doctype/asset_capitalization/asset_capitalization.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 38c8de2beea..d0e77f8868a 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -72,18 +72,13 @@ class AssetCapitalization(StockController): self.update_target_asset() def on_cancel(self): -<<<<<<< HEAD - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") -======= self.ignore_linked_doctypes = ( "GL Entry", "Stock Ledger Entry", "Repost Item Valuation", - "Serial and Batch Bundle", "Asset", ) self.cancel_target_asset() ->>>>>>> efe9f6656f (fix: Cancel asset capitalisation record on cancellation of asset and vice-versa) self.update_stock_ledger() self.make_gl_entries() self.restore_consumed_asset_items() From ac6020a940335747e25fd10f1ba0dcac7ad0e3b5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 17 Jan 2024 16:17:53 +0530 Subject: [PATCH 48/48] fix: composite asset capitalization using asset components (cherry picked from commit 5df40661d2d857eeb987e51ea6f4b465aa3d502e) --- erpnext/assets/doctype/asset/asset.py | 2 ++ .../asset_capitalization.js | 27 ++++++++++++------- .../asset_capitalization.py | 22 ++++++++++++--- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index dc1ab378eee..e5e022802d2 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1362,6 +1362,8 @@ def is_cwip_accounting_enabled(asset_category): @frappe.whitelist() def get_asset_value_after_depreciation(asset_name, finance_book=None): asset = frappe.get_doc("Asset", asset_name) + if not asset.calculate_depreciation: + return flt(asset.value_after_depreciation) return asset.get_value_after_depreciation(finance_book) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 304bdf26dee..67f8421fbde 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -20,10 +20,10 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s this.show_stock_ledger(); } - if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { - this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); - this.get_target_asset_details(); - } + // if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { + // this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); + // this.get_target_asset_details(); + // } } setup_queries() { @@ -119,13 +119,20 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s }, callback: function (r) { if (!r.exc && r.message) { - me.frm.clear_table("stock_items"); - - for (let item of r.message) { - me.frm.add_child("stock_items", item); + if(r.message[0] && r.message[0].length) { + me.frm.clear_table("stock_items"); + for (let item of r.message[0]) { + me.frm.add_child("stock_items", item); + } + refresh_field("stock_items"); + } + if (r.message[1] && r.message[1].length) { + me.frm.clear_table("asset_items"); + for (let item of r.message[1]) { + me.frm.add_child("asset_items", item); + } + me.frm.refresh_field("asset_items"); } - - refresh_field("stock_items"); me.calculate_totals(); } diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index d0e77f8868a..d1bf15eb459 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -794,7 +794,6 @@ def get_consumed_asset_details(args): out.cost_center = get_default_cost_center( args, item_defaults, item_group_defaults, brand_defaults ) - return out @@ -842,10 +841,27 @@ def get_items_tagged_to_wip_composite_asset(asset): "qty", "valuation_rate", "amount", + "is_fixed_asset", + "parent", ] pr_items = frappe.get_all( - "Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields + "Purchase Receipt Item", filters={"wip_composite_asset": asset, "docstatus": 1}, fields=fields ) - return pr_items + stock_items = [] + asset_items = [] + for d in pr_items: + if not d.is_fixed_asset: + stock_items.append(frappe._dict(d)) + else: + asset_details = frappe.db.get_value( + "Asset", + {"item_code": d.item_code, "purchase_receipt": d.parent}, + ["name as asset", "asset_name"], + as_dict=1, + ) + d.update(asset_details) + asset_items.append(frappe._dict(d)) + + return stock_items, asset_items