From 6352bfe34ebe6d96913ffb372579d52a0eba0066 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 28 Feb 2024 12:47:41 +0530 Subject: [PATCH 01/46] fix: remove free item row only if pricing rule matches (cherry picked from commit fea20db262d539b8fd5b509f8b0f84a44c42f327) --- erpnext/public/js/controllers/transaction.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 2e2b9dc300d..9f83c625f92 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1513,6 +1513,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } remove_pricing_rule_for_item(item) { + // capture pricing rule before removing it to delete free items + let removed_pricing_rule = item.pricing_rules; if (item.pricing_rules){ let me = this; return this.frm.call({ @@ -1533,7 +1535,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, callback: function(r) { if (!r.exc && r.message) { - me.remove_pricing_rule(r.message); + me.remove_pricing_rule(r.message, removed_pricing_rule); me.calculate_taxes_and_totals(); if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on"); } @@ -1791,7 +1793,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); } - remove_pricing_rule(item) { + remove_pricing_rule(item, removed_pricing_rule) { let me = this; const fields = ["discount_percentage", "discount_amount", "margin_rate_or_amount", "rate_with_margin"]; @@ -1800,7 +1802,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let items = []; me.frm.doc.items.forEach(d => { - if(d.item_code != item.remove_free_item || !d.is_free_item) { + // if same item was added a free item through a different pricing rule, keep it + if(d.item_code != item.remove_free_item || !d.is_free_item || removed_pricing_rule?.includes(d.pricing_rules)) { items.push(d); } }); From 9cb8d3392328ec123cc9a2a4f4c86433a05fb036 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 11:09:24 +0530 Subject: [PATCH 02/46] fix(setup): avoid duplicate entry for Analytics role (backport #40183) (#40185) fix(setup): avoid duplicate entry for Analytics role (#40183) (cherry picked from commit 29f91a7919210640f4d460167ee215817689ab52) Co-authored-by: Rucha Mahabal --- erpnext/setup/install.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 5b993fa64b7..ef0fb5fa802 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -20,7 +20,9 @@ default_mail_footer = """
Date: Wed, 28 Feb 2024 18:01:27 +0530 Subject: [PATCH 03/46] fix: make warning for previously existing SO an alert (cherry picked from commit 24dcd64c16eaea2f7ab0585235dfaf64cfe47bf3) --- erpnext/selling/doctype/sales_order/sales_order.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 44ffc6d1616..aafacdfa907 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -249,7 +249,8 @@ class SalesOrder(SellingController): frappe.msgprint( _("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format( frappe.bold(so[0][0]), frappe.bold(self.po_no) - ) + ), + alert=True, ) else: frappe.throw( From 23d7a1fc7687fbbfaee65a57887440471224edf4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 29 Feb 2024 17:18:42 +0530 Subject: [PATCH 04/46] fix: include Debit To/Credit To account while fetching advance (cherry picked from commit 3327599c9d5f27b10668b9e6fbef72acdcb6d5f0) --- erpnext/controllers/accounts_controller.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 89f13bbb4b4..ab72d28c600 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1133,21 +1133,24 @@ class AccountsController(TransactionBase): self.append("advances", advance_row) def get_advance_entries(self, include_unallocated=True): + party_account = [] if self.doctype == "Sales Invoice": party_type = "Customer" party = self.customer amount_field = "credit_in_account_currency" order_field = "sales_order" order_doctype = "Sales Order" + party_account.append(self.debit_to) else: party_type = "Supplier" party = self.supplier amount_field = "debit_in_account_currency" order_field = "purchase_order" order_doctype = "Purchase Order" + party_account.append(self.credit_to) - party_account = get_party_account( - party_type, party=party, company=self.company, include_advance=True + party_account.extend( + get_party_account(party_type, party=party, company=self.company, include_advance=True) ) order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field))) From a5b3c1e7cb4e3c069754d18c17b5c9449d67db7c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 29 Feb 2024 17:31:08 +0530 Subject: [PATCH 05/46] test: advance pulling logic on Sales/Purchase Invoice (cherry picked from commit 646e9ca0dd484b65d4f3bbe674b63760f8e3d5ce) --- .../sales_invoice/test_sales_invoice.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 4b7a1df44a0..6a01ccf3409 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3602,6 +3602,33 @@ class TestSalesInvoice(FrappeTestCase): check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") set_advance_flag(company="_Test Company", flag=0, default_account="") + def test_pulling_advance_based_on_debit_to(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry + + debtors2 = create_account( + parent_account="Accounts Receivable - _TC", + account_name="Debtors 2", + company="_Test Company", + account_type="Receivable", + ) + si = create_sales_invoice(do_not_submit=True) + si.debit_to = debtors2 + si.save() + + pe = create_payment_entry( + company=si.company, + payment_type="Receive", + party_type="Customer", + party=si.customer, + paid_from=debtors2, + paid_to="Cash - _TC", + paid_amount=1000, + ) + pe.submit() + advances = si.get_advance_entries() + self.assertEqual(1, len(advances)) + self.assertEqual(advances[0].reference_name, pe.name) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( From 1947a67f3d7388b1a1a7b8ff37983ddb90a20e7d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 29 Feb 2024 17:39:29 +0530 Subject: [PATCH 06/46] fix: ignore self on GL account validation for Bank Account (cherry picked from commit 3c191866545fa5e8a279030cc5314986df69f1c5) --- erpnext/accounts/doctype/bank_account/bank_account.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index 22a59bfde08..af8cae3eb84 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -57,7 +57,9 @@ class BankAccount(Document): def validate_account(self): if self.account: - if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1): + if accounts := frappe.db.get_all( + "Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1 + ): frappe.throw( _("'{0}' account is already used by {1}. Use another account.").format( frappe.bold(self.account), From 0182b95230d538c3810e93d9139829fe112a1a35 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 26 Jan 2024 21:46:19 +0530 Subject: [PATCH 07/46] fix: provisional reverse entry amount (cherry picked from commit 3e59c668064c037548c7c29c4d944efbcb37ff7b) --- .../doctype/purchase_invoice/purchase_invoice.py | 6 +++--- .../stock/doctype/purchase_receipt/purchase_receipt.py | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 2b6c6b64377..3793f97b24c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1033,7 +1033,7 @@ class PurchaseInvoice(BuyingController): purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - expense_booked_in_pr = frappe.db.get_value( + expense_booked_in_pr, credit = frappe.db.get_value( "GL Entry", { "is_cancelled": 0, @@ -1042,13 +1042,13 @@ class PurchaseInvoice(BuyingController): "voucher_detail_no": item.pr_detail, "account": provisional_account, }, - ["name"], + ["name", "credit"], ) if expense_booked_in_pr: # Intentionally passing purchase invoice item to handle partial billing purchase_receipt_doc.add_provisional_gl_entry( - item, gl_entries, self.posting_date, provisional_account, reverse=1 + item, gl_entries, self.posting_date, provisional_account, reverse=1, item_amount=credit ) if not self.is_internal_transfer(): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 67e6ff90e78..e249cf386fa 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -727,16 +727,19 @@ class PurchaseReceipt(BuyingController): ) def add_provisional_gl_entry( - self, item, gl_entries, posting_date, provisional_account, reverse=0 + self, item, gl_entries, posting_date, provisional_account, reverse=0, item_amount=None ): credit_currency = get_account_currency(provisional_account) expense_account = item.expense_account debit_currency = get_account_currency(item.expense_account) remarks = self.get("remarks") or _("Accounting Entry for Service") multiplication_factor = 1 + amount = item.base_amount if reverse: multiplication_factor = -1 + # Post reverse entry for previously posted amount + amount = item_amount expense_account = frappe.db.get_value( "Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"] ) @@ -746,7 +749,7 @@ class PurchaseReceipt(BuyingController): account=provisional_account, cost_center=item.cost_center, debit=0.0, - credit=multiplication_factor * item.base_amount, + credit=multiplication_factor * amount, remarks=remarks, against_account=expense_account, account_currency=credit_currency, @@ -760,7 +763,7 @@ class PurchaseReceipt(BuyingController): gl_entries=gl_entries, account=expense_account, cost_center=item.cost_center, - debit=multiplication_factor * item.base_amount, + debit=multiplication_factor * amount, credit=0.0, remarks=remarks, against_account=provisional_account, From 87596e6e29250ec86e2a17c846ebd5c2379b4aeb Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 29 Jan 2024 11:34:06 +0530 Subject: [PATCH 08/46] fix: handle partial invoice against provisional entry (cherry picked from commit 2a46799188a0267d76bdd292062f5a5f82429493) --- .../purchase_invoice/purchase_invoice.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3793f97b24c..139c694ba5e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1023,9 +1023,12 @@ class PurchaseInvoice(BuyingController): if provisional_accounting_for_non_stock_items: if item.purchase_receipt: - provisional_account = frappe.db.get_value( - "Purchase Receipt Item", item.pr_detail, "provisional_expense_account" - ) or self.get_company_default("default_provisional_account") + provisional_account, pr_base_rate = frappe.get_cached_value( + "Purchase Receipt Item", item.pr_detail, ["provisional_expense_account", "base_rate"] + ) + provisional_account = provisional_account or self.get_company_default( + "default_provisional_account" + ) purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) if not purchase_receipt_doc: @@ -1033,7 +1036,7 @@ class PurchaseInvoice(BuyingController): purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - expense_booked_in_pr, credit = frappe.db.get_value( + expense_booked_in_pr = frappe.db.get_value( "GL Entry", { "is_cancelled": 0, @@ -1042,13 +1045,18 @@ class PurchaseInvoice(BuyingController): "voucher_detail_no": item.pr_detail, "account": provisional_account, }, - ["name", "credit"], + "name", ) if expense_booked_in_pr: # Intentionally passing purchase invoice item to handle partial billing purchase_receipt_doc.add_provisional_gl_entry( - item, gl_entries, self.posting_date, provisional_account, reverse=1, item_amount=credit + item, + gl_entries, + self.posting_date, + provisional_account, + reverse=1, + item_amount=(item.qty * pr_base_rate), ) if not self.is_internal_transfer(): From 00d410cc1dbc110b74612c504be0df581ec274e0 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 5 Feb 2024 15:52:46 +0530 Subject: [PATCH 09/46] fix: over billing qty along with rate (cherry picked from commit cc96d2b50cbce3c137eebd754956344718cd1771) --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 139c694ba5e..0a9b9345a32 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1023,8 +1023,10 @@ class PurchaseInvoice(BuyingController): if provisional_accounting_for_non_stock_items: if item.purchase_receipt: - provisional_account, pr_base_rate = frappe.get_cached_value( - "Purchase Receipt Item", item.pr_detail, ["provisional_expense_account", "base_rate"] + provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value( + "Purchase Receipt Item", + item.pr_detail, + ["provisional_expense_account", "qty", "base_rate"], ) provisional_account = provisional_account or self.get_company_default( "default_provisional_account" @@ -1056,7 +1058,7 @@ class PurchaseInvoice(BuyingController): self.posting_date, provisional_account, reverse=1, - item_amount=(item.qty * pr_base_rate), + item_amount=(min(item.qty, pr_qty) * pr_base_rate), ) if not self.is_internal_transfer(): From e55dc2a7c08cdd26c57c9168d57f8bc07ca69c4a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 5 Feb 2024 15:55:17 +0530 Subject: [PATCH 10/46] test: overbilling for provisional accounting (cherry picked from commit ff3ca50a4b1787cd557dffefda4997a7f20cd643) --- .../purchase_invoice/test_purchase_invoice.py | 89 ++++++++++++++++--- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e4e2cd79fb4..7f1e63dfba0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1529,18 +1529,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) def test_provisional_accounting_entry(self): - create_item("_Test Non Stock Item", is_stock_item=0) - - provisional_account = create_account( - account_name="Provision Account", - parent_account="Current Liabilities - _TC", - company="_Test Company", - ) - - company = frappe.get_doc("Company", "_Test Company") - company.enable_provisional_accounting_for_non_stock_items = 1 - company.default_provisional_account = provisional_account - company.save() + setup_provisional_accounting() pr = make_purchase_receipt( item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2) @@ -1584,8 +1573,58 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date ) - company.enable_provisional_accounting_for_non_stock_items = 0 - company.save() + toggle_provisional_accounting_setting() + + def test_provisional_accounting_entry_for_over_billing(self): + setup_provisional_accounting() + + # Configure Buying Settings to allow rate change + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + # Create PR: rate = 1000, qty = 5 + pr = make_purchase_receipt( + item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2) + ) + + # Overbill PR: rate = 2000, qty = 10 + pi = create_purchase_invoice_from_receipt(pr.name) + pi.set_posting_time = 1 + pi.posting_date = add_days(pr.posting_date, -1) + pi.items[0].qty = 10 + pi.items[0].rate = 2000 + pi.items[0].expense_account = "Cost of Goods Sold - _TC" + pi.save() + pi.submit() + + expected_gle = [ + ["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, -1)], + ["Creditors - _TC", 0, 20000, add_days(pr.posting_date, -1)], + ] + + check_gl_entries(self, pi.name, expected_gle, pi.posting_date) + + expected_gle_for_purchase_receipt = [ + ["Provision Account - _TC", 5000, 0, pr.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date], + ["Provision Account - _TC", 0, 5000, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date], + ] + + check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + + # Cancel purchase invoice to check reverse provisional entry cancellation + pi.cancel() + + expected_gle_for_purchase_receipt_post_pi_cancel = [ + ["Provision Account - _TC", 0, 5000, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date], + ] + + check_gl_entries( + self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date + ) + + toggle_provisional_accounting_setting() def test_adjust_incoming_rate(self): frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) @@ -2264,4 +2303,26 @@ def make_purchase_invoice_against_cost_center(**args): return pi +def setup_provisional_accounting(**args): + args = frappe._dict(args) + create_item("_Test Non Stock Item", is_stock_item=0) + company = args.company or "_Test Company" + provisional_account = create_account( + account_name=args.account_name or "Provision Account", + parent_account=args.parent_account or "Current Liabilities - _TC", + company=company, + ) + toggle_provisional_accounting_setting( + enable=1, company=company, provisional_account=provisional_account + ) + + +def toggle_provisional_accounting_setting(**args): + args = frappe._dict(args) + company = frappe.get_doc("Company", args.company or "_Test Company") + company.enable_provisional_accounting_for_non_stock_items = args.enable or 0 + company.default_provisional_account = args.provisional_account + company.save() + + test_records = frappe.get_test_records("Purchase Invoice") From 823abfd6e0cd49e4579a5da592d17440572bbdea Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 5 Feb 2024 15:58:06 +0530 Subject: [PATCH 11/46] test: partial billing for provisional accounting (cherry picked from commit c5770f2eccc62c27f18a108ad60ae4f42946a9d6) --- .../purchase_invoice/test_purchase_invoice.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 7f1e63dfba0..e0fbef4a9b4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1626,6 +1626,45 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): toggle_provisional_accounting_setting() + def test_provisional_accounting_entry_for_partial_billing(self): + setup_provisional_accounting() + + # Configure Buying Settings to allow rate change + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + # Create PR: rate = 1000, qty = 5 + pr = make_purchase_receipt( + item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2) + ) + + # Partially bill PR: rate = 500, qty = 2 + pi = create_purchase_invoice_from_receipt(pr.name) + pi.set_posting_time = 1 + pi.posting_date = add_days(pr.posting_date, -1) + pi.items[0].qty = 2 + pi.items[0].rate = 500 + pi.items[0].expense_account = "Cost of Goods Sold - _TC" + pi.save() + pi.submit() + + expected_gle = [ + ["Cost of Goods Sold - _TC", 1000, 0, add_days(pr.posting_date, -1)], + ["Creditors - _TC", 0, 1000, add_days(pr.posting_date, -1)], + ] + + check_gl_entries(self, pi.name, expected_gle, pi.posting_date) + + expected_gle_for_purchase_receipt = [ + ["Provision Account - _TC", 5000, 0, pr.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date], + ["Provision Account - _TC", 0, 1000, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 1000, 0, pi.posting_date], + ] + + check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + + toggle_provisional_accounting_setting() + def test_adjust_incoming_rate(self): frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) From 13e334398e25a8866a3cd0e5da2b946cb7d6572f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:44:46 +0100 Subject: [PATCH 12/46] fix(Project): filter department by company (cherry picked from commit 5e736f0d061b05db8cdc0fd324fa3530079e7a07) --- erpnext/projects/doctype/project/project.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 2dac399d88f..e0dd1989e80 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -35,6 +35,14 @@ frappe.ui.form.on("Project", { }; }); + frm.set_query("department", function (doc) { + return { + filters: { + "company": doc.company, + } + }; + }); + // sales order frm.set_query('sales_order', function () { var filters = { From 85ae9eee250c5be1db00c272876dbd79b1be0038 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 22:48:15 +0530 Subject: [PATCH 13/46] fix: serial no valuation rate (backport #40221) (#40223) fix: serial no valuation rate (#40221) (cherry picked from commit a5232d9c103c36c92038622da5c1998c0fbdbe60) Co-authored-by: rohitwaghchaure --- erpnext/stock/deprecated_serial_batch.py | 41 +++---- .../serial_and_batch_bundle.py | 2 +- .../test_serial_and_batch_bundle.py | 104 ++++++++++++++++++ 3 files changed, 123 insertions(+), 24 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 2f1270e958a..ab38c151b64 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -8,9 +8,12 @@ from pypika import Order class DeprecatedSerialNoValuation: @deprecated def calculate_stock_value_from_deprecarated_ledgers(self): - serial_nos = list( - filter(lambda x: x not in self.serial_no_incoming_rate and x, self.get_serial_nos()) - ) + if not frappe.db.get_value( + "Stock Ledger Entry", {"serial_no": ("is", "set"), "is_cancelled": 0}, "name" + ): + return + + serial_nos = self.get_serial_nos() actual_qty = flt(self.sle.actual_qty) @@ -25,23 +28,12 @@ class DeprecatedSerialNoValuation: @deprecated def get_incoming_value_for_serial_nos(self, serial_nos): # get rate from serial nos within same company - all_serial_nos = frappe.get_all( - "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)} - ) - incoming_values = 0.0 - for d in all_serial_nos: - if d.company == self.sle.company: - self.serial_no_incoming_rate[d.name] += flt(d.purchase_rate) - incoming_values += flt(d.purchase_rate) - - # Get rate for serial nos which has been transferred to other company - invalid_serial_nos = [d.name for d in all_serial_nos if d.company != self.sle.company] - for serial_no in invalid_serial_nos: + for serial_no in serial_nos: table = frappe.qb.DocType("Stock Ledger Entry") - incoming_rate = ( + stock_ledgers = ( frappe.qb.from_(table) - .select(table.incoming_rate) + .select(table.incoming_rate, table.actual_qty, table.stock_value_difference) .where( ( (table.serial_no == serial_no) @@ -51,15 +43,18 @@ class DeprecatedSerialNoValuation: ) & (table.company == self.sle.company) & (table.serial_and_batch_bundle.isnull()) - & (table.actual_qty > 0) & (table.is_cancelled == 0) ) - .orderby(table.posting_date, order=Order.desc) - .limit(1) - ).run() + .orderby(table.posting_datetime, order=Order.desc) + ).run(as_dict=1) - self.serial_no_incoming_rate[serial_no] += flt(incoming_rate[0][0]) if incoming_rate else 0 - incoming_values += self.serial_no_incoming_rate[serial_no] + for sle in stock_ledgers: + self.serial_no_incoming_rate[serial_no] += ( + flt(sle.incoming_rate) + if sle.actual_qty > 0 + else (sle.stock_value_difference / sle.actual_qty) * -1 + ) + incoming_values += self.serial_no_incoming_rate[serial_no] return incoming_values diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index b6e4d6f40c6..33f0dceba94 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -2118,7 +2118,7 @@ def is_serial_batch_no_exists(item_code, type_of_transaction, serial_no=None, ba make_serial_no(serial_no, item_code) - if batch_no and frappe.db.exists("Batch", batch_no): + if batch_no and not frappe.db.exists("Batch", batch_no): if type_of_transaction != "Inward": frappe.throw(_("Batch No {0} does not exists").format(batch_no)) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index b932c1371d6..826ac03b5f7 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -540,6 +540,110 @@ class TestSerialandBatchBundle(FrappeTestCase): self.assertRaises(frappe.exceptions.ValidationError, pr2.save) + def test_serial_no_valuation_for_legacy_ledgers(self): + sn_item = make_item( + "Test Serial No Valuation for Legacy Ledgers", + properties={"has_serial_no": 1, "serial_no_series": "SNN-TSNVL.-#####"}, + ).name + + serial_nos = [] + for serial_no in [f"{sn_item}-0001", f"{sn_item}-0002"]: + if not frappe.db.exists("Serial No", serial_no): + sn_doc = frappe.get_doc( + { + "doctype": "Serial No", + "serial_no": serial_no, + "item_code": sn_item, + } + ).insert(ignore_permissions=True) + serial_nos.append(serial_no) + + frappe.flags.ignore_serial_batch_bundle_validation = True + + qty_after_transaction = 0.0 + stock_value = 0.0 + for row in [{"qty": 2, "rate": 100}, {"qty": -2, "rate": 100}, {"qty": 2, "rate": 200}]: + row = frappe._dict(row) + qty_after_transaction += row.qty + stock_value += row.rate * row.qty + + doc = frappe.get_doc( + { + "doctype": "Stock Ledger Entry", + "posting_date": today(), + "posting_time": nowtime(), + "incoming_rate": row.rate if row.qty > 0 else 0, + "qty_after_transaction": qty_after_transaction, + "stock_value_difference": row.rate * row.qty, + "stock_value": stock_value, + "valuation_rate": row.rate, + "actual_qty": row.qty, + "item_code": sn_item, + "warehouse": "_Test Warehouse - _TC", + "serial_no": "\n".join(serial_nos), + "company": "_Test Company", + } + ) + doc.flags.ignore_permissions = True + doc.flags.ignore_mandatory = True + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.submit() + + for sn in serial_nos: + sn_doc = frappe.get_doc("Serial No", sn) + if row.qty > 0: + sn_doc.db_set("warehouse", "_Test Warehouse - _TC") + else: + sn_doc.db_set("warehouse", "") + + frappe.flags.ignore_serial_batch_bundle_validation = False + + se = make_stock_entry( + item_code=sn_item, + qty=2, + source="_Test Warehouse - _TC", + serial_no="\n".join(serial_nos), + use_serial_batch_fields=True, + do_not_submit=True, + ) + + se.save() + se.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": se.name, "is_cancelled": 0, "voucher_type": "Stock Entry"}, + "stock_value_difference", + ) + + self.assertEqual(flt(stock_value_difference, 2), 400.0 * -1) + + se = make_stock_entry( + item_code=sn_item, + qty=1, + rate=353, + target="_Test Warehouse - _TC", + ) + + serial_no = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)[0] + + se = make_stock_entry( + item_code=sn_item, + qty=1, + source="_Test Warehouse - _TC", + serial_no=serial_no, + use_serial_batch_fields=True, + ) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": se.name, "is_cancelled": 0, "voucher_type": "Stock Entry"}, + "stock_value_difference", + ) + + self.assertEqual(flt(stock_value_difference, 2), 353.0 * -1) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos From b398cc657995d6efb57356fd77bb92d745ad2785 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 13:00:52 +0530 Subject: [PATCH 14/46] fix: uom wise price in sales or purchase transaction (backport #40216) (#40225) fix: uom wise price in sales or purchase transaction (#40216) fix: uom wise price (cherry picked from commit 13b05aa7fb08b14ef41b1c85860bfa4eec1d71bc) Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com> --- erpnext/public/js/controllers/transaction.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 9f83c625f92..2906e49a222 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1163,6 +1163,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, callback: function(r) { if(!r.exc) { + me.apply_price_list(item, true) frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor); } } From b785901efa6299f9e84cf038efe7f09aea1ebdad Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Feb 2024 15:34:34 +0530 Subject: [PATCH 15/46] fix: incorrect exchange rate if JE has multi parties (cherry picked from commit 694c17487d76ddc38764d58311ff2153a15d1ee5) --- .../payment_reconciliation/payment_reconciliation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index ca640151888..1bf1acee70d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -631,7 +631,12 @@ class PaymentReconciliation(Document): journals_map = frappe._dict( frappe.db.get_all( "Journal Entry Account", - filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])}, + filters={ + "parent": ("in", journals), + "account": ("in", [self.receivable_payable_account]), + "party_type": self.party_type, + "party": self.party, + }, fields=[ "parent as `name`", "exchange_rate", From 4513d83f22fad18a9fcc9a3995f70be162a58566 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 28 Feb 2024 17:07:37 +0530 Subject: [PATCH 16/46] fix: don't override reference exchange rate (cherry picked from commit eaac02655b174cca9daa2d16ba85c335e70879c3) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 7 ++++++- erpnext/accounts/utils.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 8af0713b0b4..d2ec699a6c3 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -389,7 +389,10 @@ class PaymentEntry(AccountsController): ) def set_missing_ref_details( - self, force: bool = False, update_ref_details_only_for: list | None = None + self, + force: bool = False, + update_ref_details_only_for: list | None = None, + ref_exchange_rate: float | None = None, ) -> None: for d in self.get("references"): if d.allocated_amount: @@ -401,6 +404,8 @@ class PaymentEntry(AccountsController): ref_details = get_reference_details( d.reference_doctype, d.reference_name, self.party_account_currency ) + if ref_exchange_rate: + ref_details.update({"exchange_rate": ref_exchange_rate}) for field, value in ref_details.items(): if d.exchange_gain_loss: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index c06c9774c60..b1779e655dd 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -714,7 +714,7 @@ def update_reference_in_payment_entry( payment_entry.setup_party_account_field() payment_entry.set_missing_values() if not skip_ref_details_update_for_pe: - payment_entry.set_missing_ref_details() + payment_entry.set_missing_ref_details(ref_exchange_rate=d.exchange_rate or None) payment_entry.set_amounts() payment_entry.make_exchange_gain_loss_journal( frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict From 05e4d1c240593b632eba2ad270f4adc45ad90bd9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 28 Feb 2024 17:07:57 +0530 Subject: [PATCH 17/46] test: exchange rate fetch on JE with multiple forex parties (cherry picked from commit ed95d41a51687208e603ffc355fa717018d14185) --- .../test_payment_reconciliation.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index fb75a0f7caf..301e6ef625c 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -56,6 +56,7 @@ class TestPaymentReconciliation(FrappeTestCase): self.expense_account = "Cost of Goods Sold - _PR" self.debit_to = "Debtors - _PR" self.creditors = "Creditors - _PR" + self.cash = "Cash - _PR" # create bank account if frappe.db.exists("Account", "HDFC - _PR"): @@ -486,6 +487,91 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(len(pr.get("invoices")), 0) self.assertEqual(len(pr.get("payments")), 0) + def test_payment_against_foreign_currency_journal(self): + transaction_date = nowdate() + + self.supplier = "_Test Supplier USD" + self.supplier2 = make_supplier("_Test Supplier2 USD", "USD") + amount = 100 + exc_rate1 = 80 + exc_rate2 = 83 + + je = frappe.new_doc("Journal Entry") + je.posting_date = transaction_date + je.company = self.company + je.user_remark = "test" + je.multi_currency = 1 + je.set( + "accounts", + [ + { + "account": self.creditors_usd, + "party_type": "Supplier", + "party": self.supplier, + "exchange_rate": exc_rate1, + "cost_center": self.cost_center, + "credit": amount * exc_rate1, + "credit_in_account_currency": amount, + }, + { + "account": self.creditors_usd, + "party_type": "Supplier", + "party": self.supplier2, + "exchange_rate": exc_rate2, + "cost_center": self.cost_center, + "credit": amount * exc_rate2, + "credit_in_account_currency": amount, + }, + { + "account": self.expense_account, + "cost_center": self.cost_center, + "debit": (amount * exc_rate1) + (amount * exc_rate2), + "debit_in_account_currency": (amount * exc_rate1) + (amount * exc_rate2), + }, + ], + ) + je.save().submit() + + pe = self.create_payment_entry(amount=amount, posting_date=transaction_date) + pe.payment_type = "Pay" + pe.party_type = "Supplier" + pe.party = self.supplier + pe.paid_to = self.creditors_usd + pe.paid_from = self.cash + pe.paid_amount = 8000 + pe.received_amount = 100 + pe.target_exchange_rate = exc_rate1 + pe.paid_to_account_currency = "USD" + pe.save().submit() + + pr = self.create_payment_reconciliation(party_is_customer=False) + pr.receivable_payable_account = self.creditors_usd + pr.minimum_invoice_amount = pr.maximum_invoice_amount = amount + pr.from_invoice_date = pr.to_invoice_date = transaction_date + pr.from_payment_date = pr.to_payment_date = transaction_date + + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # There should no difference_amount as the Journal and Payment have same exchange rate - 'exc_rate1' + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + + pr.reconcile() + + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 0) + self.assertEqual(len(pr.get("payments")), 0) + + journals = frappe.db.get_all( + "Journal Entry Account", + filters={"reference_type": je.doctype, "reference_name": je.name, "docstatus": 1}, + fields=["parent"], + ) + self.assertEqual([], journals) + def test_journal_against_invoice(self): transaction_date = nowdate() amount = 100 @@ -1248,3 +1334,17 @@ def make_customer(customer_name, currency=None): return customer.name else: return customer_name + + +def make_supplier(supplier_name, currency=None): + if not frappe.db.exists("Supplier", supplier_name): + supplier = frappe.new_doc("Supplier") + supplier.supplier_name = supplier_name + supplier.type = "Individual" + + if currency: + supplier.default_currency = currency + supplier.save() + return supplier.name + else: + return supplier_name From 9057bff7869035b7c79fd3c832fd066f38c9a4dd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Mar 2024 17:36:56 +0530 Subject: [PATCH 18/46] fix: make use of 'flt' to prevent really low precision exc gain/loss (cherry picked from commit 0aa72f841dbfb4a19fc6f7802172dcc5667369d7) --- erpnext/controllers/accounts_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ab72d28c600..b88148d4d5d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -46,6 +46,7 @@ from erpnext.accounts.party import ( from erpnext.accounts.utils import ( create_gain_loss_journal, get_account_currency, + get_currency_precision, get_fiscal_years, validate_fiscal_year, ) @@ -1299,10 +1300,12 @@ class AccountsController(TransactionBase): # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation. # and below logic is only for such scenarios if args: + precision = get_currency_precision() for arg in args: # Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount` if ( - arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0 + flt(arg.get("difference_amount", 0), precision) != 0 + or flt(arg.get("exchange_gain_loss", 0), precision) != 0 ) and arg.get("difference_account"): party_account = arg.get("account") From d8cf6ba38d42e401888a9018eb18afcd89d0aae9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Mar 2024 17:46:42 +0530 Subject: [PATCH 19/46] fix: allow gain/loss for Journals against Journals (cherry picked from commit 5b67631d40ddebcc6111aecb9c2f8882903d88f9) --- .../doctype/journal_entry/journal_entry.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 4537dede923..f06fafcc5cf 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -559,17 +559,28 @@ class JournalEntry(AccountsController): elif d.party_type == "Supplier" and flt(d.credit) > 0: frappe.throw(_("Row {0}: Advance against Supplier must be debit").format(d.idx)) + def system_generated_gain_loss(self): + return ( + self.voucher_type == "Exchange Gain Or Loss" + and self.multi_currency + and self.is_system_generated + ) + def validate_against_jv(self): for d in self.get("accounts"): if d.reference_type == "Journal Entry": account_root_type = frappe.get_cached_value("Account", d.account, "root_type") - if account_root_type == "Asset" and flt(d.debit) > 0: + if account_root_type == "Asset" and flt(d.debit) > 0 and not self.system_generated_gain_loss(): frappe.throw( _( "Row #{0}: For {1}, you can select reference document only if account gets credited" ).format(d.idx, d.account) ) - elif account_root_type == "Liability" and flt(d.credit) > 0: + elif ( + account_root_type == "Liability" + and flt(d.credit) > 0 + and not self.system_generated_gain_loss() + ): frappe.throw( _( "Row #{0}: For {1}, you can select reference document only if account gets debited" @@ -601,7 +612,7 @@ class JournalEntry(AccountsController): for jvd in against_entries: if flt(jvd[dr_or_cr]) > 0: valid = True - if not valid: + if not valid and not self.system_generated_gain_loss(): frappe.throw( _("Against Journal Entry {0} does not have any unmatched {1} entry").format( d.reference_name, dr_or_cr From e5fdca7b05c11ad91bf3bf0c4d4dd815e07b66b2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Mar 2024 17:53:51 +0530 Subject: [PATCH 20/46] test: gain/loss on Journals against Journals (cherry picked from commit 8a5078b826e9390bc63069fefff3cf3de8bae449) --- .../tests/test_accounts_controller.py | 78 +++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index fbdf22d5aec..65b2696e79a 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -56,7 +56,8 @@ class TestAccountsController(FrappeTestCase): 20 series - Sales Invoice against Journals 30 series - Sales Invoice against Credit Notes 40 series - Company default Cost center is unset - 50 series - Dimension inheritence + 50 series = Journals against Journals + 90 series - Dimension inheritence """ def setUp(self): @@ -1204,7 +1205,7 @@ class TestAccountsController(FrappeTestCase): x.mandatory_for_pl = False loc.save() - def test_50_dimensions_filter(self): + def test_90_dimensions_filter(self): """ Test workings of dimension filters """ @@ -1275,7 +1276,7 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 1) - def test_51_cr_note_should_inherit_dimension(self): + def test_91_cr_note_should_inherit_dimension(self): self.setup_dimensions() rate_in_account_currency = 1 @@ -1317,7 +1318,7 @@ class TestAccountsController(FrappeTestCase): frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"), ) - def test_52_dimension_inhertiance_exc_gain_loss(self): + def test_92_dimension_inhertiance_exc_gain_loss(self): # Sales Invoice in Foreign Currency self.setup_dimensions() rate = 80 @@ -1355,7 +1356,7 @@ class TestAccountsController(FrappeTestCase): ), ) - def test_53_dimension_inheritance_on_advance(self): + def test_93_dimension_inheritance_on_advance(self): self.setup_dimensions() dpt = "Research & Development" @@ -1400,3 +1401,70 @@ class TestAccountsController(FrappeTestCase): pluck="department", ), ) + + def test_50_journal_against_journal(self): + # Invoice in Foreign Currency + journal_as_invoice = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=83, + acc2=self.cash, + acc1_amount=1, + acc2_amount=83, + acc2_exc_rate=1, + ) + journal_as_invoice.accounts[0].party_type = "Customer" + journal_as_invoice.accounts[0].party = self.customer + journal_as_invoice = journal_as_invoice.save().submit() + + # Payment + journal_as_payment = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=75, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=-75, + acc2_exc_rate=1, + ) + journal_as_payment.accounts[0].party_type = "Customer" + journal_as_payment.accounts[0].party = self.customer + journal_as_payment = journal_as_payment.save().submit() + + # Reconcile the remaining amount + pr = self.create_payment_reconciliation() + # pr.receivable_payable_account = self.debit_usd + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be no outstanding in both currencies + journal_as_invoice.reload() + self.assert_ledger_outstanding(journal_as_invoice.doctype, journal_as_invoice.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(journal_as_invoice.doctype, journal_as_invoice.name) + exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual( + len(exc_je_for_si), 2 + ) # payment also has reference. so, there are 2 journals referencing invoice + self.assertEqual(len(exc_je_for_je), 1) + self.assertIn(exc_je_for_je[0], exc_je_for_si) + + # Cancel Payment + journal_as_payment.reload() + journal_as_payment.cancel() + + journal_as_invoice.reload() + self.assert_ledger_outstanding(journal_as_invoice.doctype, journal_as_invoice.name, 83.0, 1.0) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(journal_as_invoice.doctype, journal_as_invoice.name) + exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_je, []) From 1d421713bec16f1c4d2576248b15cdd47fe293c1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:50:01 +0530 Subject: [PATCH 21/46] fix: rate change on changing of the qty (backport #40241) (#40243) fix: rate change on changing of the qty (#40241) (cherry picked from commit e7d707797aeab2f5d209690c959e88851c9a6999) Co-authored-by: rohitwaghchaure --- erpnext/public/js/controllers/taxes_and_totals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index d24c4e6075d..527b762ace0 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -22,7 +22,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { item_rate = flt(item.rate_with_margin , precision("rate", item)); - if (item.discount_percentage) { + if (item.discount_percentage && !item.discount_amount) { item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100; } From 0ede99b3cb508ec88c4773b82b0a5710c2e15a52 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sun, 3 Mar 2024 18:08:23 +0530 Subject: [PATCH 22/46] feat: add company filter to child table field (cherry picked from commit 51909077bd90fe86ab3e1c916412a18e183ab823) --- .../cost_center_allocation.js | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js index ab0baab24a0..0da90161f51 100644 --- a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js +++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js @@ -3,16 +3,21 @@ frappe.ui.form.on('Cost Center Allocation', { setup: function(frm) { - let filters = {"is_group": 0}; - if (frm.doc.company) { - $.extend(filters, { - "company": frm.doc.company - }); - } - frm.set_query('main_cost_center', function() { return { - filters: filters + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('cost_center', 'allocation_percentages', function() { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } }; }); } From 689f277d13cf285ff1616ff6eb728bd5efc7aff4 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 11:54:12 +0530 Subject: [PATCH 23/46] refactor: remove balance fields from jv account --- .../journal_entry_account.json | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 3132fe9b12b..94a050a9004 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -9,12 +9,10 @@ "field_order": [ "account", "account_type", - "balance", "col_break1", "bank_account", "party_type", "party", - "party_balance", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -63,17 +61,6 @@ "label": "Account Type", "print_hide": 1 }, - { - "fieldname": "balance", - "fieldtype": "Currency", - "label": "Account Balance", - "no_copy": 1, - "oldfieldname": "balance", - "oldfieldtype": "Data", - "options": "account_currency", - "print_hide": 1, - "read_only": 1 - }, { "default": ":Company", "description": "If Income or Expense", @@ -107,14 +94,6 @@ "label": "Party", "options": "party_type" }, - { - "fieldname": "party_balance", - "fieldtype": "Currency", - "label": "Party Balance", - "options": "account_currency", - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "currency_section", "fieldtype": "Section Break", From 0c234e5c1ac77e8e9e356f6801206c029f4856a4 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 12:04:44 +0530 Subject: [PATCH 24/46] refactor: remove balance formatter --- .../doctype/journal_entry/journal_entry.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 07fb5e857ca..71920973366 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -184,7 +184,6 @@ var update_jv_details = function(doc, r) { $.each(r, function(i, d) { var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts"); frappe.model.set_value(row.doctype, row.name, "account", d.account) - frappe.model.set_value(row.doctype, row.name, "balance", d.balance) }); refresh_field("accounts"); } @@ -193,7 +192,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro onload() { this.load_defaults(); this.setup_queries(); - this.setup_balance_formatter(); erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); } @@ -292,19 +290,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro } - setup_balance_formatter() { - const formatter = function(value, df, options, doc) { - var currency = frappe.meta.get_field_currency(df, doc); - var dr_or_cr = value ? ('') : ""; - return "
" - + ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency)) - + " " + dr_or_cr - + "
"; - }; - this.frm.fields_dict.accounts.grid.update_docfield_property('balance', 'formatter', formatter); - this.frm.fields_dict.accounts.grid.update_docfield_property('party_balance', 'formatter', formatter); - } - reference_name(doc, cdt, cdn) { var d = frappe.get_doc(cdt, cdn); From c68939a0e0c11a6475f91e449cb14e228a150f25 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 12:07:36 +0530 Subject: [PATCH 25/46] refactor: exclude balance while setting acc details --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 9 ++++----- erpnext/accounts/doctype/journal_entry/journal_entry.py | 5 ++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 71920973366..bb9e4859908 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -397,11 +397,11 @@ frappe.ui.form.on("Journal Entry Account", { } }, cost_center: function(frm, dt, dn) { - erpnext.journal_entry.set_account_balance(frm, dt, dn); + erpnext.journal_entry.set_account_details(frm, dt, dn); }, account: function(frm, dt, dn) { - erpnext.journal_entry.set_account_balance(frm, dt, dn); + erpnext.journal_entry.set_account_details(frm, dt, dn); }, debit_in_account_currency: function(frm, cdt, cdn) { @@ -585,14 +585,14 @@ $.extend(erpnext.journal_entry, { }); $.extend(erpnext.journal_entry, { - set_account_balance: function(frm, dt, dn) { + set_account_details: function(frm, dt, dn) { var d = locals[dt][dn]; if(d.account) { if(!frm.doc.company) frappe.throw(__("Please select Company first")); if(!frm.doc.posting_date) frappe.throw(__("Please select Posting Date first")); return frappe.call({ - method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_account_balance_and_party_type", + method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_account_details_and_party_type", args: { account: d.account, date: frm.doc.posting_date, @@ -600,7 +600,6 @@ $.extend(erpnext.journal_entry, { debit: flt(d.debit_in_account_currency), credit: flt(d.credit_in_account_currency), exchange_rate: d.exchange_rate, - cost_center: d.cost_center }, callback: function(r) { if(r.message) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index f06fafcc5cf..be8b98b73f7 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1509,8 +1509,8 @@ def get_party_account_and_balance(company, party_type, party, cost_center=None): @frappe.whitelist() -def get_account_balance_and_party_type( - account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None +def get_account_details_and_party_type( + account, date, company, debit=None, credit=None, exchange_rate=None ): """Returns dict of account balance and party type to be set in Journal Entry on selection of account.""" if not frappe.has_permission("Account"): @@ -1532,7 +1532,6 @@ def get_account_balance_and_party_type( party_type = "" grid_values = { - "balance": get_balance_on(account, date, cost_center=cost_center), "party_type": party_type, "account_type": account_details.account_type, "account_currency": account_details.account_currency or company_currency, From 84672e2275e6c85c16212fa3393189b02071d74b Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 12:09:58 +0530 Subject: [PATCH 26/46] refactor: exclude balances while setting currency --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 3 +-- erpnext/accounts/doctype/journal_entry/journal_entry.py | 9 +-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index bb9e4859908..81b256c2249 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -385,13 +385,12 @@ frappe.ui.form.on("Journal Entry Account", { if(!d.account && d.party_type && d.party) { if(!frm.doc.company) frappe.throw(__("Please select Company")); return frm.call({ - method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_party_account_and_balance", + method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_party_account_and_currency", child: d, args: { company: frm.doc.company, party_type: d.party_type, party: d.party, - cost_center: d.cost_center } }); } diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index be8b98b73f7..a65c75c1395 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1489,21 +1489,14 @@ def get_outstanding(args): @frappe.whitelist() -def get_party_account_and_balance(company, party_type, party, cost_center=None): +def get_party_account_and_currency(company, party_type, party): if not frappe.has_permission("Account"): frappe.msgprint(_("No Permission"), raise_exception=1) account = get_party_account(party_type, party, company) - account_balance = get_balance_on(account=account, cost_center=cost_center) - party_balance = get_balance_on( - party_type=party_type, party=party, company=company, cost_center=cost_center - ) - return { "account": account, - "balance": account_balance, - "party_balance": party_balance, "account_currency": frappe.get_cached_value("Account", account, "account_currency"), } From db7348a1dee9d19bb8512e9ac4f74dc181a90b9b Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 12:13:21 +0530 Subject: [PATCH 27/46] refactor: remove controller logic for setting balances --- .../doctype/journal_entry/journal_entry.py | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index a65c75c1395..968b57ccb89 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -140,7 +140,6 @@ class JournalEntry(AccountsController): self.set_print_format_fields() self.validate_credit_debit_note() self.validate_empty_accounts_table() - self.set_account_and_party_balance() self.validate_inter_company_accounts() self.validate_depr_entry_voucher_type() @@ -1160,21 +1159,6 @@ class JournalEntry(AccountsController): if not self.get("accounts"): frappe.throw(_("Accounts table cannot be blank.")) - def set_account_and_party_balance(self): - account_balance = {} - party_balance = {} - for d in self.get("accounts"): - if d.account not in account_balance: - account_balance[d.account] = get_balance_on(account=d.account, date=self.posting_date) - - if (d.party_type, d.party) not in party_balance: - party_balance[(d.party_type, d.party)] = get_balance_on( - party_type=d.party_type, party=d.party, date=self.posting_date, company=self.company - ) - - d.account_balance = account_balance[d.account] - d.party_balance = party_balance[(d.party_type, d.party)] - @frappe.whitelist() def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None): @@ -1340,8 +1324,6 @@ def get_payment_entry(ref_doc, args): "account_type": frappe.get_cached_value("Account", args.get("party_account"), "account_type"), "account_currency": args.get("party_account_currency") or get_account_currency(args.get("party_account")), - "balance": get_balance_on(args.get("party_account")), - "party_balance": get_balance_on(party=args.get("party"), party_type=args.get("party_type")), "exchange_rate": exchange_rate, args.get("amount_field_party"): args.get("amount"), "is_advance": args.get("is_advance"), @@ -1505,7 +1487,7 @@ def get_party_account_and_currency(company, party_type, party): def get_account_details_and_party_type( account, date, company, debit=None, credit=None, exchange_rate=None ): - """Returns dict of account balance and party type to be set in Journal Entry on selection of account.""" + """Returns dict of account details and party type to be set in Journal Entry on selection of account.""" if not frappe.has_permission("Account"): frappe.msgprint(_("No Permission"), raise_exception=1) From 6c3b5bb402f4918b3234ff5c1266d02fc2e52cee Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 12:20:01 +0530 Subject: [PATCH 28/46] feat: allow on submit for selected fields --- .../doctype/journal_entry_account/journal_entry_account.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 94a050a9004..5743dc42fb1 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -39,6 +39,7 @@ ], "fields": [ { + "allow_on_submit": 1, "bold": 1, "columns": 2, "fieldname": "account", @@ -55,6 +56,7 @@ "width": "250px" }, { + "allow_on_submit": 1, "fieldname": "account_type", "fieldtype": "Data", "hidden": 1, @@ -62,6 +64,7 @@ "print_hide": 1 }, { + "allow_on_submit": 1, "default": ":Company", "description": "If Income or Expense", "fieldname": "cost_center", @@ -202,6 +205,7 @@ "no_copy": 1 }, { + "allow_on_submit": 1, "fieldname": "project", "fieldtype": "Link", "label": "Project", @@ -249,6 +253,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "fieldname": "bank_account", "fieldtype": "Link", "label": "Bank Account", From cdab3fa9709fc4bff2158ce5c6e8d860ba86959e Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 12:46:50 +0530 Subject: [PATCH 29/46] feat: update after submit in JV --- .../doctype/journal_entry/journal_entry.json | 12 +++++++++++- .../accounts/doctype/journal_entry/journal_entry.py | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 906760ec312..91febb34052 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -64,7 +64,8 @@ "stock_entry", "subscription_section", "auto_repeat", - "amended_from" + "amended_from", + "repost_required" ], "fields": [ { @@ -543,6 +544,15 @@ "label": "Is System Generated", "no_copy": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "repost_required", + "fieldtype": "Check", + "hidden": 1, + "label": "Repost Required", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-file-text", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 968b57ccb89..d01f7853371 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -172,6 +172,12 @@ class JournalEntry(AccountsController): self.update_inter_company_jv() self.update_invoice_discounting() + def on_update_after_submit(self): + if hasattr(self, "repost_required"): + child_tables = {"accounts": ("account", "account_type", "bank_account")} + self.needs_repost = self.check_if_fields_updated([], child_tables) + self.db_set("repost_required", self.needs_repost) + def on_cancel(self): # References for this Journal are removed on the `on_cancel` event in accounts_controller super(JournalEntry, self).on_cancel() From a512ec7af548d678848e5d6192226c0e2da213b4 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 12:48:14 +0530 Subject: [PATCH 30/46] refactor: better abstraction for controller code --- .../purchase_invoice/purchase_invoice.py | 1 + .../doctype/sales_invoice/sales_invoice.py | 1 + erpnext/controllers/accounts_controller.py | 31 +++++++------------ 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 0a9b9345a32..18ecd91d728 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -731,6 +731,7 @@ class PurchaseInvoice(BuyingController): "cash_bank_account", "write_off_account", "unrealized_profit_loss_account", + "is_opening", ] child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3ab9f518d05..1343b39352c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -721,6 +721,7 @@ class SalesInvoice(SellingController): "write_off_account", "loyalty_redemption_account", "unrealized_profit_loss_account", + "is_opening", ] child_tables = { "items": ("income_account", "expense_account", "discount_account"), diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b88148d4d5d..1a97245c21d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2419,27 +2419,20 @@ class AccountsController(TransactionBase): doc_before_update = self.get_doc_before_save() accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] - # Check if opening entry check updated - needs_repost = doc_before_update.get("is_opening") != self.is_opening + # Parent Level Accounts excluding party account + fields_to_check += accounting_dimensions + for field in fields_to_check: + if doc_before_update.get(field) != self.get(field): + return True - if not needs_repost: - # Parent Level Accounts excluding party account - fields_to_check += accounting_dimensions - for field in fields_to_check: - if doc_before_update.get(field) != self.get(field): - needs_repost = 1 - break + # Check for child tables + for table in child_tables: + if check_if_child_table_updated( + doc_before_update.get(table), self.get(table), child_tables[table] + ): + return True - if not needs_repost: - # Check for child tables - for table in child_tables: - needs_repost = check_if_child_table_updated( - doc_before_update.get(table), self.get(table), child_tables[table] - ) - if needs_repost: - break - - return needs_repost + return False @frappe.whitelist() def repost_accounting_entries(self): From 8585cfc533f2f2ff2191ed0da06035c55419b446 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 13:10:59 +0530 Subject: [PATCH 31/46] feat: validate before allowing repost --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index d01f7853371..1c7bafadba4 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -149,6 +149,13 @@ class JournalEntry(AccountsController): if not self.title: self.title = self.get_title() + def validate_for_repost(self): + self.validate_party() + self.validate_multi_currency() + if not frappe.flags.is_reverse_depr_entry: + self.validate_against_jv() + self.validate_stock_accounts() + def submit(self): if len(self.accounts) > 100: msgprint(_("The task has been enqueued as a background job."), alert=True) @@ -176,6 +183,7 @@ class JournalEntry(AccountsController): if hasattr(self, "repost_required"): child_tables = {"accounts": ("account", "account_type", "bank_account")} self.needs_repost = self.check_if_fields_updated([], child_tables) + self.validate_for_repost() self.db_set("repost_required", self.needs_repost) def on_cancel(self): From e5eeb216e8a52b30fd8b7138dfd83ff65b3798b9 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 13:31:15 +0530 Subject: [PATCH 32/46] feat: repost ledger button in JV --- .../doctype/journal_entry/journal_entry.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 81b256c2249..9e5a7600426 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -14,6 +14,25 @@ frappe.ui.form.on("Journal Entry", { refresh: function(frm) { erpnext.toggle_naming_series(); + if (frm.doc.repost_required && frm.doc.docstatus===1) { + frm.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update.")); + frm.add_custom_button(__('Repost Accounting Entries'), + () => { + frm.call({ + doc: frm.doc, + method: 'repost_accounting_entries', + freeze: true, + freeze_message: __('Reposting...'), + callback: (r) => { + if (!r.exc) { + frappe.msgprint(__('Accounting Entries are reposted.')); + frm.refresh(); + } + } + }); + }).removeClass('btn-default').addClass('btn-warning'); + } + if(frm.doc.docstatus > 0) { frm.add_custom_button(__('Ledger'), function() { frappe.route_options = { From 7aeb53a8060a1a725d8a39fc77d9527e89bc4553 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 15:04:56 +0530 Subject: [PATCH 33/46] refactor: use qb for JV tests --- .../journal_entry/test_journal_entry.py | 163 +++++++++--------- 1 file changed, 85 insertions(+), 78 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index a6e920b7ef6..edf0c2e7ffd 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -166,43 +166,37 @@ class TestJournalEntry(unittest.TestCase): jv.get("accounts")[1].credit_in_account_currency = 5000 jv.submit() - gl_entries = frappe.db.sql( - """select account, account_currency, debit, credit, - debit_in_account_currency, credit_in_account_currency - from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s - order by account asc""", - jv.name, - as_dict=1, - ) + self.voucher_no = jv.name - self.assertTrue(gl_entries) + self.fields = [ + "account", + "account_currency", + "debit", + "debit_in_account_currency", + "credit", + "credit_in_account_currency", + ] - expected_values = { - "_Test Bank USD - _TC": { - "account_currency": "USD", - "debit": 5000, - "debit_in_account_currency": 100, - "credit": 0, - "credit_in_account_currency": 0, - }, - "_Test Bank - _TC": { + self.expected_gle = [ + { + "account": "_Test Bank - _TC", "account_currency": "INR", "debit": 0, "debit_in_account_currency": 0, "credit": 5000, "credit_in_account_currency": 5000, }, - } + { + "account": "_Test Bank USD - _TC", + "account_currency": "USD", + "debit": 5000, + "debit_in_account_currency": 100, + "credit": 0, + "credit_in_account_currency": 0, + }, + ] - for field in ( - "account_currency", - "debit", - "debit_in_account_currency", - "credit", - "credit_in_account_currency", - ): - for i, gle in enumerate(gl_entries): - self.assertEqual(expected_values[gle.account][field], gle[field]) + self.check_gl_entries() # cancel jv.cancel() @@ -228,43 +222,37 @@ class TestJournalEntry(unittest.TestCase): rjv.posting_date = nowdate() rjv.submit() - gl_entries = frappe.db.sql( - """select account, account_currency, debit, credit, - debit_in_account_currency, credit_in_account_currency - from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s - order by account asc""", - rjv.name, - as_dict=1, - ) + self.voucher_no = rjv.name - self.assertTrue(gl_entries) + self.fields = [ + "account", + "account_currency", + "debit", + "credit", + "debit_in_account_currency", + "credit_in_account_currency", + ] - expected_values = { - "_Test Bank USD - _TC": { + self.expected_gle = [ + { + "account": "_Test Bank USD - _TC", "account_currency": "USD", "debit": 0, "debit_in_account_currency": 0, "credit": 5000, "credit_in_account_currency": 100, }, - "Sales - _TC": { + { + "account": "Sales - _TC", "account_currency": "INR", "debit": 5000, "debit_in_account_currency": 5000, "credit": 0, "credit_in_account_currency": 0, }, - } + ] - for field in ( - "account_currency", - "debit", - "debit_in_account_currency", - "credit", - "credit_in_account_currency", - ): - for i, gle in enumerate(gl_entries): - self.assertEqual(expected_values[gle.account][field], gle[field]) + self.check_gl_entries() def test_disallow_change_in_account_currency_for_a_party(self): # create jv in USD @@ -344,23 +332,25 @@ class TestJournalEntry(unittest.TestCase): jv.insert() jv.submit() - expected_values = { - "_Test Cash - _TC": {"cost_center": cost_center}, - "_Test Bank - _TC": {"cost_center": cost_center}, - } + self.voucher_no = jv.name - gl_entries = frappe.db.sql( - """select account, cost_center, debit, credit - from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s - order by account asc""", - jv.name, - as_dict=1, - ) + self.fields = [ + "account", + "cost_center", + ] - self.assertTrue(gl_entries) + self.expected_gle = [ + { + "account": "_Test Bank - _TC", + "cost_center": cost_center, + }, + { + "account": "_Test Cash - _TC", + "cost_center": cost_center, + }, + ] - for gle in gl_entries: - self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) + self.check_gl_entries() def test_jv_with_project(self): from erpnext.projects.doctype.project.test_project import make_project @@ -387,23 +377,22 @@ class TestJournalEntry(unittest.TestCase): jv.insert() jv.submit() - expected_values = { - "_Test Cash - _TC": {"project": project_name}, - "_Test Bank - _TC": {"project": project_name}, - } + self.voucher_no = jv.name - gl_entries = frappe.db.sql( - """select account, project, debit, credit - from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s - order by account asc""", - jv.name, - as_dict=1, - ) + self.fields = ["account", "project"] - self.assertTrue(gl_entries) + self.expected_gle = [ + { + "account": "_Test Bank - _TC", + "project": project_name, + }, + { + "account": "_Test Cash - _TC", + "project": project_name, + }, + ] - for gle in gl_entries: - self.assertEqual(expected_values[gle.account]["project"], gle.project) + self.check_gl_entries() def test_jv_account_and_party_balance_with_cost_centre(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center @@ -426,6 +415,24 @@ class TestJournalEntry(unittest.TestCase): account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center) self.assertEqual(expected_account_balance, account_balance) + def check_gl_entries(self): + gl = frappe.qb.DocType("GL Entry") + query = frappe.qb.from_(gl) + for field in self.fields: + query = query.select(gl[field]) + + query = query.where( + (gl.voucher_type == "Journal Entry") + & (gl.voucher_no == self.voucher_no) + & (gl.is_cancelled == 0) + ).orderby(gl.account) + + gl_entries = query.run(as_dict=True) + + for i in range(len(self.expected_gle)): + for field in self.fields: + self.assertEqual(self.expected_gle[i][field], gl_entries[i][field]) + def make_journal_entry( account1, From b0be08f05dbc2197033765ec00d55132ebbf555c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 15:06:04 +0530 Subject: [PATCH 34/46] test: reposting entries for JV --- .../journal_entry/test_journal_entry.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index edf0c2e7ffd..7106d9a1bcf 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -415,6 +415,68 @@ class TestJournalEntry(unittest.TestCase): account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center) self.assertEqual(expected_account_balance, account_balance) + def test_repost_accounting_entries(self): + from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + + jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False) + jv.multi_currency = 0 + jv.submit() + + self.voucher_no = jv.name + + self.fields = [ + "account", + "debit_in_account_currency", + "credit_in_account_currency", + "cost_center", + ] + + self.expected_gle = [ + { + "account": "_Test Bank - _TC", + "debit_in_account_currency": 0, + "credit_in_account_currency": 100, + "cost_center": "_Test Cost Center - _TC", + }, + { + "account": "_Test Cash - _TC", + "debit_in_account_currency": 100, + "credit_in_account_currency": 0, + "cost_center": "_Test Cost Center - _TC", + }, + ] + + self.check_gl_entries() + + cost_center = "_Test Cost Center for BS Account - _TC" + create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") + + jv.accounts[0].account = "_Test Bank - _TC" + jv.accounts[0].cost_center = "_Test Cost Center for BS Account - _TC" + jv.accounts[1].account = "_Test Cash - _TC" + + jv.save() + jv.load_from_db() + self.assertTrue(jv.repost_required) + jv.repost_accounting_entries() + + self.expected_gle = [ + { + "account": "_Test Bank - _TC", + "debit_in_account_currency": 100, + "credit_in_account_currency": 0, + "cost_center": "_Test Cost Center for BS Account - _TC", + }, + { + "account": "_Test Cash - _TC", + "debit_in_account_currency": 0, + "credit_in_account_currency": 100, + "cost_center": "_Test Cost Center - _TC", + }, + ] + + self.check_gl_entries() + def check_gl_entries(self): gl = frappe.qb.DocType("GL Entry") query = frappe.qb.from_(gl) From 617365409334003f7d55dd2d30ec9085e0c98b22 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 15:57:38 +0530 Subject: [PATCH 35/46] chore: correct typo --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 9e5a7600426..72469ea1e11 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -15,7 +15,7 @@ frappe.ui.form.on("Journal Entry", { erpnext.toggle_naming_series(); if (frm.doc.repost_required && frm.doc.docstatus===1) { - frm.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update.")); + frm.set_intro(__("Accounting entries for this Journal Entry need to be reposted. Please click on 'Repost' button to update.")); frm.add_custom_button(__('Repost Accounting Entries'), () => { frm.call({ From e20c1acb6ee377252aaa295d384e2e1d7646252a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 16:54:41 +0530 Subject: [PATCH 36/46] fix: check child rows before update --- erpnext/controllers/accounts_controller.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1a97245c21d..c250756d4f0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3458,17 +3458,14 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil def check_if_child_table_updated( child_table_before_update, child_table_after_update, fields_to_check ): - accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] + fields_to_check = list(fields_to_check) + get_accounting_dimensions() + ["cost_center", "project"] + # Check if any field affecting accounting entry is altered - for index, item in enumerate(child_table_after_update): + for index, item in enumerate(child_table_before_update): for field in fields_to_check: if child_table_before_update[index].get(field) != item.get(field): return True - for dimension in accounting_dimensions: - if child_table_before_update[index].get(dimension) != item.get(dimension): - return True - return False From 6230bbc77d64ef41d1b06b8781a0be358435e599 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 12 Oct 2023 17:40:57 +0530 Subject: [PATCH 37/46] fix: test for reposting pi --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c250756d4f0..045e4fb7b6b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3463,7 +3463,7 @@ def check_if_child_table_updated( # Check if any field affecting accounting entry is altered for index, item in enumerate(child_table_before_update): for field in fields_to_check: - if child_table_before_update[index].get(field) != item.get(field): + if child_table_after_update[index].get(field) != item.get(field): return True return False From c55c0f7173b61fce91b803ec6283b440e504280a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 8 Feb 2024 15:35:10 +0530 Subject: [PATCH 38/46] fix: disable editable account heads --- .../accounts/doctype/journal_entry/journal_entry.py | 13 +++---------- .../journal_entry_account.json | 5 +---- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 1c7bafadba4..b85b7f8b1e4 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -149,13 +149,6 @@ class JournalEntry(AccountsController): if not self.title: self.title = self.get_title() - def validate_for_repost(self): - self.validate_party() - self.validate_multi_currency() - if not frappe.flags.is_reverse_depr_entry: - self.validate_against_jv() - self.validate_stock_accounts() - def submit(self): if len(self.accounts) > 100: msgprint(_("The task has been enqueued as a background job."), alert=True) @@ -181,9 +174,9 @@ class JournalEntry(AccountsController): def on_update_after_submit(self): if hasattr(self, "repost_required"): - child_tables = {"accounts": ("account", "account_type", "bank_account")} - self.needs_repost = self.check_if_fields_updated([], child_tables) - self.validate_for_repost() + self.needs_repost = self.check_if_fields_updated( + fields_to_check=[], child_tables={"accounts": []} + ) self.db_set("repost_required", self.needs_repost) def on_cancel(self): diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 5743dc42fb1..00fd7b9d0ab 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -39,7 +39,6 @@ ], "fields": [ { - "allow_on_submit": 1, "bold": 1, "columns": 2, "fieldname": "account", @@ -56,7 +55,6 @@ "width": "250px" }, { - "allow_on_submit": 1, "fieldname": "account_type", "fieldtype": "Data", "hidden": 1, @@ -253,7 +251,6 @@ "fieldtype": "Column Break" }, { - "allow_on_submit": 1, "fieldname": "bank_account", "fieldtype": "Link", "label": "Bank Account", @@ -271,7 +268,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-23 11:44:25.841187", + "modified": "2024-02-05 01:10:50.224840", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", From cd24a2a05d5dfb361ae83b6dccf803048c6f9514 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 8 Feb 2024 16:09:28 +0530 Subject: [PATCH 39/46] fix: allow editable accounting dimensions for repostable doctypes --- .../accounting_dimension/accounting_dimension.py | 6 ++++++ .../accounts/doctype/journal_entry/journal_entry.py | 12 +++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 295cd983b3d..5258214dad2 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -11,6 +11,10 @@ from frappe.model import core_doctypes_list from frappe.model.document import Document from frappe.utils import cstr +from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( + get_allowed_types_from_settings, +) + class AccountingDimension(Document): # begin: auto-generated types @@ -106,6 +110,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None): doc_count = len(get_accounting_dimensions()) count = 0 + repostable_doctypes = get_allowed_types_from_settings() for doctype in doclist: @@ -121,6 +126,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None): "options": doc.document_type, "insert_after": insert_after_field, "owner": "Administrator", + "allow_on_submit": 1 if doctype in repostable_doctypes else 0, } meta = frappe.get_meta(doctype, cached=False) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index b85b7f8b1e4..d5b9393ac08 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -13,6 +13,10 @@ from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import ( get_party_account_based_on_invoice_discounting, ) +from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( + validate_docs_for_deferred_accounting, + validate_docs_for_voucher_types, +) from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) @@ -149,6 +153,10 @@ class JournalEntry(AccountsController): if not self.title: self.title = self.get_title() + def validate_for_repost(self): + validate_docs_for_voucher_types(["Journal Entry"]) + validate_docs_for_deferred_accounting([self.name], []) + def submit(self): if len(self.accounts) > 100: msgprint(_("The task has been enqueued as a background job."), alert=True) @@ -177,7 +185,9 @@ class JournalEntry(AccountsController): self.needs_repost = self.check_if_fields_updated( fields_to_check=[], child_tables={"accounts": []} ) - self.db_set("repost_required", self.needs_repost) + if self.needs_repost: + self.validate_for_repost() + self.db_set("repost_required", self.needs_repost) def on_cancel(self): # References for this Journal are removed on the `on_cancel` event in accounts_controller From 4b5712688a21f4f9e38c7bee27d4f01dd1ee13f4 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 8 Feb 2024 16:36:30 +0530 Subject: [PATCH 40/46] feat: add patch for making repostable dimension fields editable --- erpnext/patches.txt | 1 + ...on_submit_dimensions_for_repostable_doctypes.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 erpnext/patches/v15_0/allow_on_submit_dimensions_for_repostable_doctypes.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f0b104e2c5f..d5c0a4e80e4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -353,6 +353,7 @@ erpnext.patches.v14_0.update_zero_asset_quantity_field execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") erpnext.patches.v14_0.update_total_asset_cost_field erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool +erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 diff --git a/erpnext/patches/v15_0/allow_on_submit_dimensions_for_repostable_doctypes.py b/erpnext/patches/v15_0/allow_on_submit_dimensions_for_repostable_doctypes.py new file mode 100644 index 00000000000..e75610d0a53 --- /dev/null +++ b/erpnext/patches/v15_0/allow_on_submit_dimensions_for_repostable_doctypes.py @@ -0,0 +1,14 @@ +import frappe + +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, +) +from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( + get_allowed_types_from_settings, +) + + +def execute(): + for dt in get_allowed_types_from_settings(): + for dimension in get_accounting_dimensions(): + frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1) From 16349553c7ae96351935d9b04a472151df22358a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 9 Feb 2024 13:11:51 +0530 Subject: [PATCH 41/46] fix: test for repost accounting in JVs --- .../journal_entry/test_journal_entry.py | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 7106d9a1bcf..798d3bb6c82 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -418,10 +418,18 @@ class TestJournalEntry(unittest.TestCase): def test_repost_accounting_entries(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + # Configure Repost Accounting Ledger for JVs + settings = frappe.get_doc("Repost Accounting Ledger Settings") + if not [x for x in settings.allowed_types if x.document_type == "Journal Entry"]: + settings.append("allowed_types", {"document_type": "Journal Entry", "allowed": True}) + settings.save() + + # Create JV with defaut cost center - _Test Cost Center jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False) jv.multi_currency = 0 jv.submit() + # Check GL entries before reposting self.voucher_no = jv.name self.fields = [ @@ -448,33 +456,18 @@ class TestJournalEntry(unittest.TestCase): self.check_gl_entries() - cost_center = "_Test Cost Center for BS Account - _TC" + # Change cost center for bank account - _Test Cost Center for BS Account create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - - jv.accounts[0].account = "_Test Bank - _TC" - jv.accounts[0].cost_center = "_Test Cost Center for BS Account - _TC" - jv.accounts[1].account = "_Test Cash - _TC" - + jv.accounts[1].cost_center = "_Test Cost Center for BS Account - _TC" jv.save() - jv.load_from_db() + + # Check if repost flag gets set on update after submit self.assertTrue(jv.repost_required) jv.repost_accounting_entries() - self.expected_gle = [ - { - "account": "_Test Bank - _TC", - "debit_in_account_currency": 100, - "credit_in_account_currency": 0, - "cost_center": "_Test Cost Center for BS Account - _TC", - }, - { - "account": "_Test Cash - _TC", - "debit_in_account_currency": 0, - "credit_in_account_currency": 100, - "cost_center": "_Test Cost Center - _TC", - }, - ] - + # Check GL entries after reposting + jv.load_from_db() + self.expected_gle[0]["cost_center"] = "_Test Cost Center for BS Account - _TC" self.check_gl_entries() def check_gl_entries(self): From 93f3af7dbabe4ad3a690057be0478aeda0388896 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:13:49 +0530 Subject: [PATCH 42/46] perf: serial and batch bundle valuation (reposting) (backport #40255) (#40262) * perf: serial and batch bundle valuation (reposting) (#40255) perf: serial and batch bundle valuation (cherry picked from commit 6379238893e94fa19da65b08428000e77fa50bd6) * chore: fix styles * chore: fix linters --------- Co-authored-by: rohitwaghchaure --- erpnext/stock/deprecated_serial_batch.py | 29 +++++++++++---- .../test_landed_cost_voucher.py | 17 --------- .../serial_and_batch_bundle.py | 7 +--- erpnext/stock/serial_batch_bundle.py | 36 ++++++++++--------- erpnext/stock/stock_ledger.py | 19 +++++++++- 5 files changed, 61 insertions(+), 47 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index ab38c151b64..1aa8134b485 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -13,7 +13,9 @@ class DeprecatedSerialNoValuation: ): return - serial_nos = self.get_serial_nos() + serial_nos = self.get_filterd_serial_nos() + if not serial_nos: + return actual_qty = flt(self.sle.actual_qty) @@ -25,8 +27,21 @@ class DeprecatedSerialNoValuation: self.stock_value_change += stock_value_change + def get_filterd_serial_nos(self): + serial_nos = [] + non_filtered_serial_nos = self.get_serial_nos() + + # If the serial no inwarded using the Serial and Batch Bundle, then the serial no should not be considered + for serial_no in non_filtered_serial_nos: + if serial_no and serial_no not in self.serial_no_incoming_rate: + serial_nos.append(serial_no) + + return serial_nos + @deprecated def get_incoming_value_for_serial_nos(self, serial_nos): + from erpnext.stock.utils import get_combine_datetime + # get rate from serial nos within same company incoming_values = 0.0 for serial_no in serial_nos: @@ -42,18 +57,20 @@ class DeprecatedSerialNoValuation: | (table.serial_no.like("%\n" + serial_no + "\n%")) ) & (table.company == self.sle.company) + & (table.warehouse == self.sle.warehouse) & (table.serial_and_batch_bundle.isnull()) + & (table.actual_qty > 0) & (table.is_cancelled == 0) + & ( + table.posting_datetime <= get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + ) ) .orderby(table.posting_datetime, order=Order.desc) + .limit(1) ).run(as_dict=1) for sle in stock_ledgers: - self.serial_no_incoming_rate[serial_no] += ( - flt(sle.incoming_rate) - if sle.actual_qty > 0 - else (sle.stock_value_difference / sle.actual_qty) * -1 - ) + self.serial_no_incoming_rate[serial_no] += flt(sle.incoming_rate) incoming_values += self.serial_no_incoming_rate[serial_no] return incoming_values diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 257f263bd22..4058aa82d7e 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -415,23 +415,6 @@ class TestLandedCostVoucher(FrappeTestCase): create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges) new_purchase_rate = serial_no_rate + charges - sn_obj = SerialNoValuation( - sle=frappe._dict( - { - "posting_date": today(), - "posting_time": nowtime(), - "item_code": "_Test Serialized Item", - "warehouse": "Stores - TCP1", - "serial_nos": [serial_no], - } - ) - ) - - new_serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no) - - # Since the serial no is already delivered the rate must be zero - self.assertFalse(new_serial_no_rate) - stock_value_difference = frappe.db.get_value( "Stock Ledger Entry", filters={ diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 33f0dceba94..d01dfefc926 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -332,13 +332,8 @@ class SerialandBatchBundle(Document): rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) for d in self.entries: - if not rate or ( - flt(rate, precision) == flt(d.incoming_rate, precision) and d.stock_value_difference - ): - continue - d.incoming_rate = flt(rate, precision) - if self.has_batch_no: + if d.qty: d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) if save: diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 24dd9d1d203..1fcc439fda3 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -4,8 +4,9 @@ from typing import List import frappe from frappe import _, bold from frappe.model.naming import make_autoname -from frappe.query_builder.functions import CombineDatetime, Sum +from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp from frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today +from pypika import Order from erpnext.stock.deprecated_serial_batch import ( DeprecatedBatchNoValuation, @@ -424,19 +425,21 @@ class SerialNoValuation(DeprecatedSerialNoValuation): ) else: - entries = self.get_serial_no_ledgers() - self.serial_no_incoming_rate = defaultdict(float) self.stock_value_change = 0.0 - for ledger in entries: - self.stock_value_change += ledger.incoming_rate - self.serial_no_incoming_rate[ledger.serial_no] += ledger.incoming_rate + serial_nos = self.get_serial_nos() + for serial_no in serial_nos: + incoming_rate = self.get_incoming_rate_from_bundle(serial_no) + if not incoming_rate: + continue + + self.stock_value_change += incoming_rate + self.serial_no_incoming_rate[serial_no] += incoming_rate self.calculate_stock_value_from_deprecarated_ledgers() - def get_serial_no_ledgers(self): - serial_nos = self.get_serial_nos() + def get_incoming_rate_from_bundle(self, serial_no) -> float: bundle = frappe.qb.DocType("Serial and Batch Bundle") bundle_child = frappe.qb.DocType("Serial and Batch Entry") @@ -444,20 +447,18 @@ class SerialNoValuation(DeprecatedSerialNoValuation): frappe.qb.from_(bundle) .inner_join(bundle_child) .on(bundle.name == bundle_child.parent) - .select( - bundle.name, - bundle_child.serial_no, - (bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"), - ) + .select((bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate")) .where( (bundle.is_cancelled == 0) & (bundle.docstatus == 1) - & (bundle_child.serial_no.isin(serial_nos)) - & (bundle.type_of_transaction.isin(["Inward", "Outward"])) + & (bundle_child.serial_no == serial_no) + & (bundle.type_of_transaction == "Inward") + & (bundle_child.qty > 0) & (bundle.item_code == self.sle.item_code) & (bundle_child.warehouse == self.sle.warehouse) ) - .orderby(bundle.posting_date, bundle.posting_time, bundle.creation) + .orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc) + .limit(1) ) # Important to exclude the current voucher to calculate correct the stock value difference @@ -474,7 +475,8 @@ class SerialNoValuation(DeprecatedSerialNoValuation): query = query.where(timestamp_condition) - return query.run(as_dict=True) + incoming_rate = query.run() + return flt(incoming_rate[0][0]) if incoming_rate else 0.0 def get_serial_nos(self): if self.sle.get("serial_nos"): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index fa5938d5226..90c41f8e3f2 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -952,7 +952,12 @@ class update_entries_after(object): get_rate_for_return, # don't move this import to top ) - if self.valuation_method == "Moving Average": + if ( + self.valuation_method == "Moving Average" + and not sle.get("serial_no") + and not sle.get("batch_no") + and not sle.get("serial_and_batch_bundle") + ): rate = get_incoming_rate( { "item_code": sle.item_code, @@ -979,6 +984,18 @@ class update_entries_after(object): voucher_detail_no=sle.voucher_detail_no, sle=sle, ) + + if ( + sle.get("serial_and_batch_bundle") + and rate > 0 + and sle.voucher_type in ["Delivery Note", "Sales Invoice"] + ): + frappe.db.set_value( + sle.voucher_type + " Item", + sle.voucher_detail_no, + "incoming_rate", + rate, + ) elif ( sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and sle.voucher_detail_no From 63209f4eac3a317b8358cf394d7997148e0532da Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:14:21 +0530 Subject: [PATCH 43/46] fix: do not allow to cancel incomplete reposting (backport #40224) (#40230) * fix: do not allow to cancel incomplete reposting (#40224) (cherry picked from commit 72ac56b6c4366910f7fc4f561811240ad0694965) # Conflicts: # erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py * chore: fix conflicts --------- Co-authored-by: rohitwaghchaure --- .../repost_item_valuation/repost_item_valuation.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 13937d928b8..8a73cb6b37b 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -219,14 +219,9 @@ class RepostItemValuation(Document): if self.status not in ("Queued", "In Progress"): return - if not (self.voucher_no and self.voucher_no): - return - - transaction_status = frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") - if transaction_status == 2: - msg = _("Cannot cancel as processing of cancelled documents is pending.") - msg += "
" + _("Please try again in an hour.") - frappe.throw(msg, title=_("Pending processing")) + msg = _("Cannot cancel as processing of cancelled documents is pending.") + msg += "
" + _("Please try again in an hour.") + frappe.throw(msg, title=_("Pending processing")) @frappe.whitelist() def restart_reposting(self): From c7b96df69c627c76d293deac182986403cf70942 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 20:03:42 +0530 Subject: [PATCH 44/46] fix: report path from the Item and Putaway Rule list (backport #40190) (#40266) fix: report path from the Item and Putaway Rule list (#40190) (cherry picked from commit 8e2f9787c1fbc98930750d34ce592d2fb56fcc1e) Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com> --- erpnext/stock/doctype/item/item_list.js | 3 +-- erpnext/stock/doctype/putaway_rule/putaway_rule_list.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/item/item_list.js b/erpnext/stock/doctype/item/item_list.js index 22d38e88935..3946a4291d3 100644 --- a/erpnext/stock/doctype/item/item_list.js +++ b/erpnext/stock/doctype/item/item_list.js @@ -18,8 +18,7 @@ frappe.listview_settings['Item'] = { reports: [ { name: 'Stock Summary', - report_type: 'Page', - route: 'stock-balance' + route: '/app/stock-balance' }, { name: 'Stock Ledger', diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js index 725e91ee8d9..753569c8812 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -11,8 +11,7 @@ frappe.listview_settings['Putaway Rule'] = { reports: [ { name: 'Warehouse Capacity Summary', - report_type: 'Page', - route: 'warehouse-capacity-summary' + route: '/app/warehouse-capacity-summary' } ] }; From ee119f30fbf18d3e3b65681a95a6558366438994 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 5 Mar 2024 14:32:36 +0530 Subject: [PATCH 45/46] chore: fixed fetch from (backport #40270) (#40275) chore: fixed fetch from (#40270) (cherry picked from commit 34051bc04f93c808066da469dd61a706684ef4f8) Co-authored-by: rohitwaghchaure --- .../asset_capitalization_service_item.json | 5 +++-- .../asset_capitalization_stock_item.json | 4 ++-- erpnext/stock/doctype/delivery_note/delivery_note.json | 9 ++------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json index 0ae1c1428ee..31c9d52bcaa 100644 --- a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json +++ b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json @@ -63,7 +63,7 @@ }, { "columns": 1, - "fetch_from": "stock_item_code.stock_uom", + "fetch_from": "item_code.stock_uom", "fieldname": "uom", "fieldtype": "Link", "in_list_view": 1, @@ -110,7 +110,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-08 15:52:08.598100", + "modified": "2024-03-05 11:23:40.766844", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization Service Item", @@ -118,5 +118,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json index f79a84855d5..c838f8b0f26 100644 --- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json @@ -65,7 +65,7 @@ }, { "columns": 1, - "fetch_from": "stock_item_code.stock_uom", + "fetch_from": "item_code.stock_uom", "fieldname": "stock_uom", "fieldtype": "Link", "in_list_view": 1, @@ -178,7 +178,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-02-25 15:57:35.007501", + "modified": "2024-03-05 11:22:57.346889", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization Stock Item", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 7873d3e6de4..d07a825331d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -28,7 +28,6 @@ "column_break_18", "project", "dimension_col_break", - "custom_dimensions_section", "currency_and_price_list", "currency", "conversion_rate", @@ -927,7 +926,7 @@ "width": "50%" }, { - "fetch_from": "transporter.name", + "fetch_from": "transporter.supplier_name", "fieldname": "transporter_name", "fieldtype": "Data", "label": "Transporter Name", @@ -1355,10 +1354,6 @@ "fieldname": "column_break_43", "fieldtype": "Column Break" }, - { - "fieldname": "custom_dimensions_section", - "fieldtype": "Section Break" - }, { "fieldname": "shipping_address_section", "fieldtype": "Section Break", @@ -1402,7 +1397,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2023-12-18 17:19:39.368239", + "modified": "2024-03-05 11:58:47.784349", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", From d74647d5a589feacbbf9f4bea66a6abc94344b8a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Mar 2024 14:46:24 +0530 Subject: [PATCH 46/46] fix: incorrect TCS on customer and suppliers with same name (cherry picked from commit 9904a9868c346028fa3b1809ac575ea1e1c6632b) --- .../tax_withholding_category/tax_withholding_category.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index c39a9db9800..405f58715bd 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -546,6 +546,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): "GL Entry", { "is_cancelled": 0, + "party_type": "Customer", "party": ["in", parties], "company": inv.company, "voucher_no": ["in", vouchers], @@ -560,6 +561,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): conditions = [] conditions.append(ple.amount.lt(0)) conditions.append(ple.delinked == 0) + conditions.append(ple.party_type == "Customer") conditions.append(ple.party.isin(parties)) conditions.append(ple.voucher_no == ple.against_voucher_no) conditions.append(ple.company == inv.company) @@ -579,6 +581,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): { "is_cancelled": 0, "credit": [">", 0], + "party_type": "Customer", "party": ["in", parties], "posting_date": ["between", (tax_details.from_date, tax_details.to_date)], "company": inv.company,