From 3a57ef4c5fe0849b32e92e27415b3ae06bb2d363 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 5 Sep 2024 14:38:22 +0530 Subject: [PATCH 01/26] refactor: Handle Emp Advance as separate row in AP report (cherry picked from commit eedf22b07a86c0c8ff6763e94f7f541482714fa5) --- .../accounts_receivable.py | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 11177a10772..643dbc174a1 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -112,6 +112,26 @@ class ReceivablePayableReport: self.build_data() + def build_voucher_dict(self, ple): + return frappe._dict( + voucher_type=ple.voucher_type, + voucher_no=ple.voucher_no, + party=ple.party, + party_account=ple.account, + posting_date=ple.posting_date, + account_currency=ple.account_currency, + remarks=ple.remarks, + invoiced=0.0, + paid=0.0, + credit_note=0.0, + outstanding=0.0, + invoiced_in_account_currency=0.0, + paid_in_account_currency=0.0, + credit_note_in_account_currency=0.0, + outstanding_in_account_currency=0.0, + cost_center=ple.cost_center, + ) + def init_voucher_balance(self): # build all keys, since we want to exclude vouchers beyond the report date for ple in self.ple_entries: @@ -123,24 +143,8 @@ class ReceivablePayableReport: key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) if key not in self.voucher_balance: - self.voucher_balance[key] = frappe._dict( - voucher_type=ple.voucher_type, - voucher_no=ple.voucher_no, - party=ple.party, - party_account=ple.account, - posting_date=ple.posting_date, - account_currency=ple.account_currency, - remarks=ple.remarks, - invoiced=0.0, - paid=0.0, - credit_note=0.0, - outstanding=0.0, - invoiced_in_account_currency=0.0, - paid_in_account_currency=0.0, - credit_note_in_account_currency=0.0, - outstanding_in_account_currency=0.0, - cost_center=ple.cost_center, - ) + self.voucher_balance[key] = self.build_voucher_dict(ple) + self.get_invoices(ple) if self.filters.get("group_by_party"): @@ -208,6 +212,14 @@ class ReceivablePayableReport: row = self.voucher_balance.get(key) + # Build and use a separate row for Employee Advances. + # This allows Payments or Journals made against Emp Advance to be processed. + if not row and ple.against_voucher_type == "Employee Advance": + _d = self.build_voucher_dict(ple) + _d.voucher_type = ple.against_voucher_type + _d.voucher_no = ple.against_voucher_no + row = self.voucher_balance[key] = _d + if not row: # no invoice, this is an invoice / stand-alone payment / credit note if self.filters.get("ignore_accounts"): From 2913785bacbb54f65ede732b5d728cbc7a6925ea Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 5 Sep 2024 16:12:52 +0530 Subject: [PATCH 02/26] refactor: filter to toggle employee advance scenario in AP (cherry picked from commit 257e13c2997a0d2fc7682ae9b7b5feca837a63dc) --- .../accounts/report/accounts_payable/accounts_payable.js | 5 +++++ .../report/accounts_receivable/accounts_receivable.py | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 61a3a96d5fe..171a94e1151 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -162,6 +162,11 @@ frappe.query_reports["Accounts Payable"] = { label: __("Group by Voucher"), fieldtype: "Check", }, + { + fieldname: "handle_employee_advances", + label: __("Handle Employee Advances"), + fieldtype: "Check", + }, ], formatter: function (value, row, column, data, default_formatter) { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 643dbc174a1..b8341bcfa01 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -214,7 +214,11 @@ class ReceivablePayableReport: # Build and use a separate row for Employee Advances. # This allows Payments or Journals made against Emp Advance to be processed. - if not row and ple.against_voucher_type == "Employee Advance": + if ( + not row + and ple.against_voucher_type == "Employee Advance" + and self.filters.handle_employee_advances + ): _d = self.build_voucher_dict(ple) _d.voucher_type = ple.against_voucher_type _d.voucher_no = ple.against_voucher_no From 3691a500ebde0367d51f0bb18d2b7a060df0bd50 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 27 Aug 2024 16:53:34 +0530 Subject: [PATCH 03/26] fix: `default_advance_account` field in Process Payment Reconciliation (cherry picked from commit 143209f91a201c58004408cd04812aefa49e9f94) # Conflicts: # erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json # erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py --- .../process_payment_reconciliation.js | 20 ++++++++++++- .../process_payment_reconciliation.json | 18 ++++++++++- .../process_payment_reconciliation.py | 30 +++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js index 0f52a4d998e..9c0a14bd177 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js @@ -20,6 +20,17 @@ frappe.ui.form.on("Process Payment Reconciliation", { }, }; }); + + frm.set_query("default_advance_account", function (doc) { + return { + filters: { + company: doc.company, + is_group: 0, + account_type: doc.party_type == "Customer" ? "Receivable" : "Payable", + root_type: doc.party_type == "Customer" ? "Liability" : "Asset", + }, + }; + }); frm.set_query("cost_center", function (doc) { return { filters: { @@ -102,6 +113,7 @@ frappe.ui.form.on("Process Payment Reconciliation", { company(frm) { frm.set_value("party", ""); frm.set_value("receivable_payable_account", ""); + frm.set_value("default_advance_account", ""); }, party_type(frm) { frm.set_value("party", ""); @@ -109,6 +121,7 @@ frappe.ui.form.on("Process Payment Reconciliation", { party(frm) { frm.set_value("receivable_payable_account", ""); + frm.set_value("default_advance_account", ""); if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) { return frappe.call({ method: "erpnext.accounts.party.get_party_account", @@ -119,7 +132,12 @@ frappe.ui.form.on("Process Payment Reconciliation", { }, callback: (r) => { if (!r.exc && r.message) { - frm.set_value("receivable_payable_account", r.message); + if (typeof r.message === "string") { + frm.set_value("receivable_payable_account", r.message); + } else if (Array.isArray(r.message)) { + frm.set_value("receivable_payable_account", r.message[0]); + frm.set_value("default_advance_account", r.message[1]); + } } frm.refresh(); }, diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json index 1a1ab4d800e..b0584dd4c4d 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json @@ -13,6 +13,7 @@ "column_break_io6c", "party", "receivable_payable_account", + "default_advance_account", "filter_section", "from_invoice_date", "to_invoice_date", @@ -141,12 +142,27 @@ { "fieldname": "section_break_a8yx", "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.party", + "description": "Only 'Payment Entries' made against this advance account are supported.", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account", + "fieldname": "default_advance_account", + "fieldtype": "Link", + "label": "Default Advance Account", + "mandatory_depends_on": "doc.party_type", + "options": "Account", + "reqd": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-08-11 10:56:51.699137", +======= + "modified": "2024-08-27 14:48:56.715320", +>>>>>>> 143209f91a (fix: `default_advance_account` field in Process Payment Reconciliation) "modified_by": "Administrator", "module": "Accounts", "name": "Process Payment Reconciliation", @@ -180,4 +196,4 @@ "sort_order": "DESC", "states": [], "title_field": "company" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py index b1419020f0f..e92d8ed9d30 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py @@ -11,6 +11,35 @@ from frappe.utils.scheduler import is_scheduler_inactive class ProcessPaymentReconciliation(Document): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + amended_from: DF.Link | None + bank_cash_account: DF.Link | None + company: DF.Link + cost_center: DF.Link | None + default_advance_account: DF.Link + error_log: DF.LongText | None + from_invoice_date: DF.Date | None + from_payment_date: DF.Date | None + party: DF.DynamicLink + party_type: DF.Link + receivable_payable_account: DF.Link + status: DF.Literal[ + "", "Queued", "Running", "Paused", "Completed", "Partially Reconciled", "Failed", "Cancelled" + ] + to_invoice_date: DF.Date | None + to_payment_date: DF.Date | None + # end: auto-generated types + +>>>>>>> 143209f91a (fix: `default_advance_account` field in Process Payment Reconciliation) def validate(self): self.validate_receivable_payable_account() self.validate_bank_cash_account() @@ -76,6 +105,7 @@ def get_pr_instance(doc: str): "party_type", "party", "receivable_payable_account", + "default_advance_account", "from_invoice_date", "to_invoice_date", "from_payment_date", From 202ebbe140f4e634bdd7aeddd7f388ef5c0692db Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 3 Sep 2024 17:27:50 +0530 Subject: [PATCH 04/26] refactor: fetch advance account on party seleection (cherry picked from commit c4ed04cb31a80e694a3c27ec92e4e574f2035d39) --- .../process_payment_reconciliation.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js index 9c0a14bd177..d72c4724690 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js @@ -129,6 +129,7 @@ frappe.ui.form.on("Process Payment Reconciliation", { company: frm.doc.company, party_type: frm.doc.party_type, party: frm.doc.party, + include_advance: 1, }, callback: (r) => { if (!r.exc && r.message) { From f5069524f35a70e1934f14e4c4f7f40f1346d497 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Thu, 5 Sep 2024 19:13:31 +0530 Subject: [PATCH 05/26] fix: cancel common party advance jv while canceling the invoice (cherry picked from commit 6a928b92dfa90de8290a3e5829deb69a80c6ccdb) --- erpnext/accounts/utils.py | 40 ++++++++++++++++++++++ erpnext/controllers/accounts_controller.py | 3 ++ 2 files changed, 43 insertions(+) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 0ae4db3673c..b2c4d92a6af 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -739,6 +739,46 @@ def cancel_exchange_gain_loss_journal( gain_loss_je.cancel() +def cancel_common_party_journal(self): + if self.doctype not in ["Sales Invoice", "Purchase Invoice"]: + return + + if not frappe.db.get_single_value("Accounts Settings", "enable_common_party_accounting"): + return + + party_link = self.get_common_party_link() + if not party_link: + return + + journal_entry = frappe.db.get_value( + "Journal Entry Account", + filters={ + "reference_type": self.doctype, + "reference_name": self.name, + "docstatus": 1, + }, + fieldname="parent", + ) + + if not journal_entry: + return + + common_party_journal = frappe.db.get_value( + "Journal Entry", + filters={ + "name": journal_entry, + "is_system_generated": True, + "docstatus": 1, + }, + ) + + if not common_party_journal: + return + + common_party_je = frappe.get_doc("Journal Entry", common_party_journal) + common_party_je.cancel() + + def update_accounting_ledgers_after_reference_removal( ref_type: str | None = None, ref_no: str | None = None, payment_name: str | None = None ): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b8218ce8ddc..1b624d3dd1a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1463,6 +1463,7 @@ class AccountsController(TransactionBase): remove_from_bank_transaction, ) from erpnext.accounts.utils import ( + cancel_common_party_journal, cancel_exchange_gain_loss_journal, unlink_ref_doc_from_payment_entries, ) @@ -1474,6 +1475,7 @@ class AccountsController(TransactionBase): # Cancel Exchange Gain/Loss Journal before unlinking cancel_exchange_gain_loss_journal(self) + cancel_common_party_journal(self) if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"): unlink_ref_doc_from_payment_entries(self) @@ -2302,6 +2304,7 @@ class AccountsController(TransactionBase): jv.posting_date = self.posting_date jv.company = self.company jv.remark = f"Adjustment for {self.doctype} {self.name}" + jv.is_system_generated = True reconcilation_entry = frappe._dict() advance_entry = frappe._dict() From e7b17e05b079c7de59c273fb22f197395818eda0 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Thu, 5 Sep 2024 23:30:35 +0530 Subject: [PATCH 06/26] test: add unit test for canceling the common party advance jv created from sales invoice (cherry picked from commit 8c6e3f3c12164f30f044b3bf1fa0b71b66ff837d) --- .../sales_invoice/test_sales_invoice.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 7b471ed418e..f5f905e33ec 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3113,6 +3113,50 @@ class TestSalesInvoice(FrappeTestCase): party_link.delete() frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0) + def test_sales_invoice_cancel_with_common_party_advance_jv(self): + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + from erpnext.accounts.doctype.party_link.party_link import create_party_link + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + # create a customer + customer = make_customer(customer="_Test Common Supplier") + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Supplier").name + + # create a party link between customer & supplier + party_link = create_party_link("Supplier", supplier, customer) + + # enable common party accounting + frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1) + + # create a sales invoice + si = create_sales_invoice(customer=customer) + + # check creation of journal entry + jv = frappe.db.get_value( + "Journal Entry Account", + filters={ + "reference_type": si.doctype, + "reference_name": si.name, + "docstatus": 1, + }, + fieldname="parent", + ) + + self.assertTrue(jv) + + # cancel sales invoice + si.cancel() + + # check cancellation of journal entry + jv_status = frappe.db.get_value("Journal Entry", jv, "docstatus") + self.assertEqual(jv_status, 2) + + party_link.delete() + frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0) + def test_payment_statuses(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry From 77892f4e249f999feda5df937031364b4245d984 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 7 Sep 2024 17:34:22 +0530 Subject: [PATCH 07/26] fix: incorrect qty after transaction in SLE (backport #43103) (#43104) fix: incorrect qty after transaction in SLE (#43103) (cherry picked from commit 5ff87edc85b6bf1a355a44650440055abbb75ef5) Co-authored-by: rohitwaghchaure --- erpnext/stock/stock_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 826d8eb0060..63e5d858b5a 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1237,7 +1237,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc voucher_no = args.get("voucher_no") voucher_condition = f"and voucher_no != '{voucher_no}'" - elif args.get("creation"): + elif args.get("creation") and args.get("sle_id"): creation = args.get("creation") operator = "<=" voucher_condition = f"and creation < '{creation}'" From 9e77a0245a8a2dcea168437fdbbd9ddedc0064e7 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 9 Sep 2024 06:51:30 +0530 Subject: [PATCH 08/26] chore: resolve conflicts with backport --- .../process_payment_reconciliation.json | 4 --- .../process_payment_reconciliation.py | 30 ------------------- 2 files changed, 34 deletions(-) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json index b0584dd4c4d..0511571d754 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json @@ -158,11 +158,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-08-11 10:56:51.699137", -======= "modified": "2024-08-27 14:48:56.715320", ->>>>>>> 143209f91a (fix: `default_advance_account` field in Process Payment Reconciliation) "modified_by": "Administrator", "module": "Accounts", "name": "Process Payment Reconciliation", diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py index e92d8ed9d30..d3e6123972a 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py @@ -1,4 +1,3 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt import json @@ -11,35 +10,6 @@ from frappe.utils.scheduler import is_scheduler_inactive class ProcessPaymentReconciliation(Document): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - amended_from: DF.Link | None - bank_cash_account: DF.Link | None - company: DF.Link - cost_center: DF.Link | None - default_advance_account: DF.Link - error_log: DF.LongText | None - from_invoice_date: DF.Date | None - from_payment_date: DF.Date | None - party: DF.DynamicLink - party_type: DF.Link - receivable_payable_account: DF.Link - status: DF.Literal[ - "", "Queued", "Running", "Paused", "Completed", "Partially Reconciled", "Failed", "Cancelled" - ] - to_invoice_date: DF.Date | None - to_payment_date: DF.Date | None - # end: auto-generated types - ->>>>>>> 143209f91a (fix: `default_advance_account` field in Process Payment Reconciliation) def validate(self): self.validate_receivable_payable_account() self.validate_bank_cash_account() From 5113b0063ed44345a5d3821d5f7330d62c0f3c30 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 9 Sep 2024 06:58:34 +0530 Subject: [PATCH 09/26] chore: resolve conflicts with backport --- .../process_payment_reconciliation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py index d3e6123972a..5bbb2a14b92 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py @@ -1,3 +1,4 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt import json From a3a40febf370a1537b971891419786a0f67877a8 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Fri, 6 Sep 2024 19:53:40 +0530 Subject: [PATCH 10/26] fix: check multi-currency on jv for common party accounting with foreign currency (cherry picked from commit 00938bfd4d1243b570e83f13f5774029ad0df3a1) --- erpnext/controllers/accounts_controller.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1b624d3dd1a..a8a790719bf 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2298,6 +2298,8 @@ class AccountsController(TransactionBase): primary_account = get_party_account(primary_party_type, primary_party, self.company) secondary_account = get_party_account(secondary_party_type, secondary_party, self.company) + primary_account_currency = get_account_currency(primary_account) + secondary_account_currency = get_account_currency(secondary_account) jv = frappe.new_doc("Journal Entry") jv.voucher_type = "Journal Entry" @@ -2338,6 +2340,10 @@ class AccountsController(TransactionBase): advance_entry.credit_in_account_currency = self.outstanding_amount reconcilation_entry.debit_in_account_currency = self.outstanding_amount + default_currency = erpnext.get_company_currency(self.company) + if primary_account_currency != default_currency or secondary_account_currency != default_currency: + jv.multi_currency = 1 + jv.append("accounts", reconcilation_entry) jv.append("accounts", advance_entry) From b6f352a02470bc7034b9e05e8ea142fbebeb97c8 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Fri, 6 Sep 2024 20:01:35 +0530 Subject: [PATCH 11/26] test: add unit test for common party with foreign currency (cherry picked from commit 740a04a70437422dac2b824d058b264026228b79) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../sales_invoice/test_sales_invoice.py | 196 +++++++++++++++++- 1 file changed, 195 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f5f905e33ec..9677660c5f9 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -44,7 +44,7 @@ class TestSalesInvoice(FrappeTestCase): from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) - create_internal_parties() + # create_internal_parties() setup_accounts() frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) @@ -3679,6 +3679,7 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(res[0][0], pos_return.return_against) +<<<<<<< HEAD def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( """select account, debit, credit, posting_date @@ -3688,6 +3689,199 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1, +======= + def _create_opening_roundoff_account(self, company_name): + liability_root = frappe.db.get_all( + "Account", + filters={"company": company_name, "root_type": "Liability", "disabled": 0}, + order_by="lft", + limit=1, + )[0] + + # setup round off account + if acc := frappe.db.exists( + "Account", + { + "account_name": "Round Off for Opening", + "account_type": "Round Off for Opening", + "company": company_name, + }, + ): + frappe.db.set_value("Company", company_name, "round_off_for_opening", acc) + else: + acc = frappe.new_doc("Account") + acc.company = company_name + acc.parent_account = liability_root.name + acc.account_name = "Round Off for Opening" + acc.account_type = "Round Off for Opening" + acc.save() + frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name) + + def test_opening_invoice_with_rounding_adjustment(self): + si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True) + si.is_opening = "Yes" + si.items[0].income_account = "Temporary Opening - _TC" + si.save() + + self._create_opening_roundoff_account(si.company) + + si.reload() + si.submit() + res = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": si.name, "is_opening": "Yes"}, + fields=["account", "debit", "credit", "is_opening"], + ) + self.assertEqual(len(res), 3) + + def _create_opening_invoice_with_inclusive_tax(self): + si = create_sales_invoice(qty=1, rate=90, do_not_submit=True) + si.is_opening = "Yes" + si.items[0].income_account = "Temporary Opening - _TC" + item_template = si.items[0].as_dict() + item_template.name = None + item_template.rate = 55 + si.append("items", item_template) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Testing...", + "rate": 5, + "included_in_print_rate": True, + }, + ) + # there will be 0.01 precision loss between Dr and Cr + # caused by 'included_in_print_tax' option + si.save() + return si + + def test_rounding_validation_for_opening_with_inclusive_tax(self): + si = self._create_opening_invoice_with_inclusive_tax() + # 'Round Off for Opening' not set in Company master + # Ledger level validation must be thrown + self.assertRaises(frappe.ValidationError, si.submit) + + def test_ledger_entries_on_opening_invoice_with_rounding_loss_by_inclusive_tax(self): + si = self._create_opening_invoice_with_inclusive_tax() + # 'Round Off for Opening' is set in Company master + self._create_opening_roundoff_account(si.company) + + si.submit() + actual = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": si.name, "is_opening": "Yes", "is_cancelled": False}, + fields=["account", "debit", "credit", "is_opening"], + order_by="account,debit", + ) + expected = [ + {"account": "_Test Account Service Tax - _TC", "debit": 0.0, "credit": 6.9, "is_opening": "Yes"}, + {"account": "Debtors - _TC", "debit": 145.0, "credit": 0.0, "is_opening": "Yes"}, + {"account": "Round Off for Opening - _TC", "debit": 0.0, "credit": 0.01, "is_opening": "Yes"}, + {"account": "Temporary Opening - _TC", "debit": 0.0, "credit": 138.09, "is_opening": "Yes"}, + ] + self.assertEqual(len(actual), 4) + self.assertEqual(expected, actual) + + def test_common_party_with_foreign_currency_jv(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + from erpnext.accounts.doctype.party_link.party_link import create_party_link + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + from erpnext.setup.utils import get_exchange_rate + + creditors = create_account( + account_name="Creditors USD", + parent_account="Accounts Payable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Payable", + ) + debtors = create_account( + account_name="Debtors USD", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Receivable", + ) + + # create a customer + customer = make_customer(customer="_Test Common Party USD") + cust_doc = frappe.get_doc("Customer", customer) + cust_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": debtors, + } + cust_doc.append("accounts", test_account_details) + cust_doc.save() + + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Party USD").name + supp_doc = frappe.get_doc("Supplier", supplier) + supp_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": creditors, + } + supp_doc.append("accounts", test_account_details) + supp_doc.save() + + # enable common party accounting + frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1) + + # create a party link between customer & supplier + party_link = create_party_link("Supplier", supplier, customer) + + # create a sales invoice + si = create_sales_invoice( + customer=customer, + currency="USD", + conversion_rate=get_exchange_rate("USD", "INR"), + debit_to=debtors, + do_not_save=1, + ) + si.party_account_currency = "USD" + si.save() + si.submit() + + # check outstanding of sales invoice + si.reload() + self.assertEqual(si.status, "Paid") + self.assertEqual(flt(si.outstanding_amount), 0.0) + + # check creation of journal entry + jv = frappe.get_all( + "Journal Entry Account", + { + "account": si.debit_to, + "party_type": "Customer", + "party": si.customer, + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="credit_in_account_currency", + ) + self.assertTrue(jv) + self.assertEqual(jv[0], si.grand_total) + + party_link.delete() + frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0) + + +def set_advance_flag(company, flag, default_account): + frappe.db.set_value( + "Company", + company, + { + "book_advance_payments_in_separate_party_account": flag, + "default_advance_received_account": default_account, + }, +>>>>>>> 740a04a704 (test: add unit test for common party with foreign currency) ) for i, gle in enumerate(gl_entries): From 4dd06b69a1eac3d0b490a8d78f156bdba684c052 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Fri, 6 Sep 2024 20:03:56 +0530 Subject: [PATCH 12/26] fix: uncomment internal parties (cherry picked from commit 454e18ad5fef1ad81aab3efcca1d7886a0d80fbf) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 9677660c5f9..9cf2bf77248 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -44,7 +44,7 @@ class TestSalesInvoice(FrappeTestCase): from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) - # create_internal_parties() + create_internal_parties() setup_accounts() frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) From de0b8c07f6c4522ad8c1825943151a8c7a70e157 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 7 Sep 2024 11:39:06 +0530 Subject: [PATCH 13/26] refactor(test): use change_settings decorator (cherry picked from commit ee94fb37c81a28e89aa2175b933d231a5a6601f7) --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 9cf2bf77248..e0318ebbf2a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3785,6 +3785,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): self.assertEqual(len(actual), 4) self.assertEqual(expected, actual) + @change_settings("Accounts Settings", {"enable_common_party_accounting": True}) def test_common_party_with_foreign_currency_jv(self): from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( @@ -3831,11 +3832,8 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): supp_doc.append("accounts", test_account_details) supp_doc.save() - # enable common party accounting - frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1) - # create a party link between customer & supplier - party_link = create_party_link("Supplier", supplier, customer) + create_party_link("Supplier", supplier, customer) # create a sales invoice si = create_sales_invoice( @@ -3869,9 +3867,6 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): self.assertTrue(jv) self.assertEqual(jv[0], si.grand_total) - party_link.delete() - frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0) - def set_advance_flag(company, flag, default_account): frappe.db.set_value( From cf78f9702c64ff6fb87cc011af59b9fa721a11c7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 9 Sep 2024 11:01:54 +0530 Subject: [PATCH 14/26] chore: resolve conflict --- .../sales_invoice/test_sales_invoice.py | 125 ++---------------- 1 file changed, 9 insertions(+), 116 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index e0318ebbf2a..9b303430455 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3678,113 +3678,6 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(len(res), 1) self.assertEqual(res[0][0], pos_return.return_against) - -<<<<<<< HEAD -def check_gl_entries(doc, voucher_no, expected_gle, posting_date): - gl_entries = frappe.db.sql( - """select account, debit, credit, posting_date - from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s - and is_cancelled = 0 - order by posting_date asc, account asc""", - (voucher_no, posting_date), - as_dict=1, -======= - def _create_opening_roundoff_account(self, company_name): - liability_root = frappe.db.get_all( - "Account", - filters={"company": company_name, "root_type": "Liability", "disabled": 0}, - order_by="lft", - limit=1, - )[0] - - # setup round off account - if acc := frappe.db.exists( - "Account", - { - "account_name": "Round Off for Opening", - "account_type": "Round Off for Opening", - "company": company_name, - }, - ): - frappe.db.set_value("Company", company_name, "round_off_for_opening", acc) - else: - acc = frappe.new_doc("Account") - acc.company = company_name - acc.parent_account = liability_root.name - acc.account_name = "Round Off for Opening" - acc.account_type = "Round Off for Opening" - acc.save() - frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name) - - def test_opening_invoice_with_rounding_adjustment(self): - si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True) - si.is_opening = "Yes" - si.items[0].income_account = "Temporary Opening - _TC" - si.save() - - self._create_opening_roundoff_account(si.company) - - si.reload() - si.submit() - res = frappe.db.get_all( - "GL Entry", - filters={"voucher_no": si.name, "is_opening": "Yes"}, - fields=["account", "debit", "credit", "is_opening"], - ) - self.assertEqual(len(res), 3) - - def _create_opening_invoice_with_inclusive_tax(self): - si = create_sales_invoice(qty=1, rate=90, do_not_submit=True) - si.is_opening = "Yes" - si.items[0].income_account = "Temporary Opening - _TC" - item_template = si.items[0].as_dict() - item_template.name = None - item_template.rate = 55 - si.append("items", item_template) - si.append( - "taxes", - { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Testing...", - "rate": 5, - "included_in_print_rate": True, - }, - ) - # there will be 0.01 precision loss between Dr and Cr - # caused by 'included_in_print_tax' option - si.save() - return si - - def test_rounding_validation_for_opening_with_inclusive_tax(self): - si = self._create_opening_invoice_with_inclusive_tax() - # 'Round Off for Opening' not set in Company master - # Ledger level validation must be thrown - self.assertRaises(frappe.ValidationError, si.submit) - - def test_ledger_entries_on_opening_invoice_with_rounding_loss_by_inclusive_tax(self): - si = self._create_opening_invoice_with_inclusive_tax() - # 'Round Off for Opening' is set in Company master - self._create_opening_roundoff_account(si.company) - - si.submit() - actual = frappe.db.get_all( - "GL Entry", - filters={"voucher_no": si.name, "is_opening": "Yes", "is_cancelled": False}, - fields=["account", "debit", "credit", "is_opening"], - order_by="account,debit", - ) - expected = [ - {"account": "_Test Account Service Tax - _TC", "debit": 0.0, "credit": 6.9, "is_opening": "Yes"}, - {"account": "Debtors - _TC", "debit": 145.0, "credit": 0.0, "is_opening": "Yes"}, - {"account": "Round Off for Opening - _TC", "debit": 0.0, "credit": 0.01, "is_opening": "Yes"}, - {"account": "Temporary Opening - _TC", "debit": 0.0, "credit": 138.09, "is_opening": "Yes"}, - ] - self.assertEqual(len(actual), 4) - self.assertEqual(expected, actual) - @change_settings("Accounts Settings", {"enable_common_party_accounting": True}) def test_common_party_with_foreign_currency_jv(self): from erpnext.accounts.doctype.account.test_account import create_account @@ -3868,15 +3761,15 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): self.assertEqual(jv[0], si.grand_total) -def set_advance_flag(company, flag, default_account): - frappe.db.set_value( - "Company", - company, - { - "book_advance_payments_in_separate_party_account": flag, - "default_advance_received_account": default_account, - }, ->>>>>>> 740a04a704 (test: add unit test for common party with foreign currency) +def check_gl_entries(doc, voucher_no, expected_gle, posting_date): + gl_entries = frappe.db.sql( + """select account, debit, credit, posting_date + from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s + and is_cancelled = 0 + order by posting_date asc, account asc""", + (voucher_no, posting_date), + as_dict=1, ) for i, gle in enumerate(gl_entries): From 36f65fc5922a6227a9347dd79a63c2e50b869092 Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <141945075+Nihantra-Patel@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:27:59 +0530 Subject: [PATCH 15/26] fix: set today in 'On This Date' in Available Batch Report (cherry picked from commit 9fd55e4c83e4c0b715dd746c5032ac51217641d6) --- .../report/available_batch_report/available_batch_report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.js b/erpnext/stock/report/available_batch_report/available_batch_report.js index 011f7e09ca2..a13e4ca82f7 100644 --- a/erpnext/stock/report/available_batch_report/available_batch_report.js +++ b/erpnext/stock/report/available_batch_report/available_batch_report.js @@ -17,7 +17,7 @@ frappe.query_reports["Available Batch Report"] = { fieldtype: "Date", width: "80", reqd: 1, - default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + default: frappe.datetime.get_today(), }, { fieldname: "item_code", From 53496ed79fd299836206662e6e30b82a6ca309ce Mon Sep 17 00:00:00 2001 From: Prashant Kamble <99401472+pra17shant@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:21:05 +0000 Subject: [PATCH 16/26] fix: unreconcile allocation child table redirect url voucher no issue (cherry picked from commit 5d6f6a2fb972e4678fe55f5d3970903501f10a10) --- erpnext/public/js/utils/unreconcile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index 6864e2865d3..c6ee8a330c7 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -69,7 +69,7 @@ erpnext.accounts.unreconcile_payment = { { label: __("Voucher Type"), fieldname: "voucher_type", - fieldtype: "Dynamic Link", + fieldtype: "Link", options: "DocType", in_list_view: 1, read_only: 1, @@ -77,7 +77,7 @@ erpnext.accounts.unreconcile_payment = { { label: __("Voucher No"), fieldname: "voucher_no", - fieldtype: "Link", + fieldtype: "Dynamic Link", options: "voucher_type", in_list_view: 1, read_only: 1, From 5e33961448ba4b0f781f675137e1c891aad51bd2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 17 Aug 2024 02:27:36 +0200 Subject: [PATCH 17/26] fix(Delivery Note): translatability of validation errors (cherry picked from commit 34df6e39dcca685b6b107c5f0631721d7156768c) --- .../doctype/delivery_note/delivery_note.py | 70 ++++++++----------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 18adf6ab1f9..b791568666c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -211,52 +211,42 @@ class DeliveryNote(SellingController): self.validate_sales_invoice_references() def validate_sales_order_references(self): - err_msg = "" + errors = [] for item in self.items: - if (item.against_sales_order and not item.so_detail) or ( - not item.against_sales_order and item.so_detail - ): - if not item.against_sales_order: - err_msg += ( - _("'Sales Order' reference ({1}) is missing in row {0}").format( - frappe.bold(item.idx), frappe.bold("against_sales_order") - ) - + "
" - ) - else: - err_msg += ( - _("'Sales Order Item' reference ({1}) is missing in row {0}").format( - frappe.bold(item.idx), frappe.bold("so_detail") - ) - + "
" - ) + missing_label = None + if item.against_sales_order and not item.so_detail: + missing_label = item.meta.get_label("so_detail") + elif item.so_detail and not item.against_sales_order: + missing_label = item.meta.get_label("against_sales_order") - if err_msg: - frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete")) + if missing_label and missing_label != "No Label": + errors.append( + _("The field {0} in row {1} is not set").format( + frappe.bold(_(missing_label)), frappe.bold(item.idx) + ) + ) + + if errors: + frappe.throw("
".join(errors), title=_("References to Sales Orders are Incomplete")) def validate_sales_invoice_references(self): - err_msg = "" + errors = [] for item in self.items: - if (item.against_sales_invoice and not item.si_detail) or ( - not item.against_sales_invoice and item.si_detail - ): - if not item.against_sales_invoice: - err_msg += ( - _("'Sales Invoice' reference ({1}) is missing in row {0}").format( - frappe.bold(item.idx), frappe.bold("against_sales_invoice") - ) - + "
" - ) - else: - err_msg += ( - _("'Sales Invoice Item' reference ({1}) is missing in row {0}").format( - frappe.bold(item.idx), frappe.bold("si_detail") - ) - + "
" - ) + missing_label = None + if item.against_sales_invoice and not item.si_detail: + missing_label = item.meta.get_label("si_detail") + elif item.si_detail and not item.against_sales_invoice: + missing_label = item.meta.get_label("against_sales_invoice") - if err_msg: - frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete")) + if missing_label and missing_label != "No Label": + errors.append( + _("The field {0} in row {1} is not set").format( + frappe.bold(_(missing_label)), frappe.bold(item.idx) + ) + ) + + if errors: + frappe.throw("
".join(errors), title=_("References to Sales Invoices are Incomplete")) def validate_proj_cust(self): """check for does customer belong to same project as entered..""" From 14ae3edc1d0b811683b51d0084213fdf5a433baa Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 17 Aug 2024 02:36:16 +0200 Subject: [PATCH 18/26] refactor: extract common validation method (cherry picked from commit 08646b7ab7605aa76b1defe691c97070dc54c317) --- .../doctype/delivery_note/delivery_note.py | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index b791568666c..b725eff05b7 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -211,32 +211,23 @@ class DeliveryNote(SellingController): self.validate_sales_invoice_references() def validate_sales_order_references(self): - errors = [] - for item in self.items: - missing_label = None - if item.against_sales_order and not item.so_detail: - missing_label = item.meta.get_label("so_detail") - elif item.so_detail and not item.against_sales_order: - missing_label = item.meta.get_label("against_sales_order") - - if missing_label and missing_label != "No Label": - errors.append( - _("The field {0} in row {1} is not set").format( - frappe.bold(_(missing_label)), frappe.bold(item.idx) - ) - ) - - if errors: - frappe.throw("
".join(errors), title=_("References to Sales Orders are Incomplete")) + self._validate_dependent_item_fields( + "against_sales_order", "so_detail", _("References to Sales Orders are Incomplete") + ) def validate_sales_invoice_references(self): + self._validate_dependent_item_fields( + "against_sales_invoice", "si_detail", _("References to Sales Invoices are Incomplete") + ) + + def _validate_dependent_item_fields(self, field_a: str, field_b: str, error_title: str): errors = [] for item in self.items: missing_label = None - if item.against_sales_invoice and not item.si_detail: - missing_label = item.meta.get_label("si_detail") - elif item.si_detail and not item.against_sales_invoice: - missing_label = item.meta.get_label("against_sales_invoice") + if item.get(field_a) and not item.get(field_b): + missing_label = item.meta.get_label(field_b) + elif item.get(field_b) and not item.get(field_a): + missing_label = item.meta.get_label(field_a) if missing_label and missing_label != "No Label": errors.append( @@ -246,7 +237,7 @@ class DeliveryNote(SellingController): ) if errors: - frappe.throw("
".join(errors), title=_("References to Sales Invoices are Incomplete")) + frappe.throw("
".join(errors), title=error_title) def validate_proj_cust(self): """check for does customer belong to same project as entered..""" From a4b9dda4b2c66ed526817a946be263ffced56b5a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:35:14 +0530 Subject: [PATCH 19/26] fix: incorrect actual cost in Procurement Tracker report (backport #43109) (#43137) fix: incorrect actual cost in Procurement Tracker report (#43109) (cherry picked from commit 80f101f92e2bd85927450b6adb073c575a327f42) Co-authored-by: rohitwaghchaure --- .../buying/report/procurement_tracker/procurement_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index a7e03c08fac..bd0798236b3 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -175,7 +175,7 @@ def get_data(filters): "purchase_order": po.parent, "supplier": po.supplier, "estimated_cost": flt(mr_record.get("amount")), - "actual_cost": flt(pi_records.get(po.name)), + "actual_cost": flt(pi_records.get(po.name)) or flt(po.amount), "purchase_order_amt": flt(po.amount), "purchase_order_amt_in_company_currency": flt(po.base_amount), "expected_delivery_date": po.schedule_date, From e8aae5018a14f3646ca5f7078c5a7379c853858e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:26:21 +0200 Subject: [PATCH 20/26] fix: return type of `get_party_details` (backport #43131) (#43135) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> fix: return type of `get_party_details` (#43131) --- erpnext/accounts/party.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 8240b79bfdc..a19eedd8b72 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -68,7 +68,7 @@ def get_party_details( pos_profile=None, ): if not party: - return {} + return frappe._dict() if not frappe.db.exists(party_type, party): frappe.throw(_("{0}: {1} does not exists").format(party_type, party)) return _get_party_details( From 37b8715096df6a7e645fedd40bc42e869857fcd7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 9 Sep 2024 12:31:32 +0530 Subject: [PATCH 21/26] feat: utility report to identify invalid ledger entries (cherry picked from commit 832c4aaf82e6556a83429a3757efb3449b53de2a) --- .../report/invalid_ledger_entries/__init__.py | 0 .../invalid_ledger_entries.js | 13 +++++ .../invalid_ledger_entries.json | 23 +++++++++ .../invalid_ledger_entries.py | 48 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 erpnext/accounts/report/invalid_ledger_entries/__init__.py create mode 100644 erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js create mode 100644 erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json create mode 100644 erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py diff --git a/erpnext/accounts/report/invalid_ledger_entries/__init__.py b/erpnext/accounts/report/invalid_ledger_entries/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js new file mode 100644 index 00000000000..548a6f7d951 --- /dev/null +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js @@ -0,0 +1,13 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Invalid Ledger Entries"] = { + filters: [ + // { + // "fieldname": "my_filter", + // "label": __("My Filter"), + // "fieldtype": "Data", + // "reqd": 1, + // }, + ], +}; diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json new file mode 100644 index 00000000000..00dbbfc5056 --- /dev/null +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json @@ -0,0 +1,23 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2024-09-09 12:31:25.295976", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2024-09-09 12:31:25.295976", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Invalid Ledger Entries", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "Invalid Ledger Entries", + "report_type": "Script Report", + "roles": [], + "timeout": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py new file mode 100644 index 00000000000..4f4b1835227 --- /dev/null +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py @@ -0,0 +1,48 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe import _ + + +def execute(filters: dict | None = None): + """Return columns and data for the report. + + This is the main entry point for the report. It accepts the filters as a + dictionary and should return columns and data. It is called by the framework + every time the report is refreshed or a filter is updated. + """ + columns = get_columns() + data = get_data() + + return columns, data + + +def get_columns() -> list[dict]: + """Return columns for the report. + + One field definition per column, just like a DocType field definition. + """ + return [ + { + "label": _("Column 1"), + "fieldname": "column_1", + "fieldtype": "Data", + }, + { + "label": _("Column 2"), + "fieldname": "column_2", + "fieldtype": "Int", + }, + ] + + +def get_data() -> list[list]: + """Return data for the report. + + The report data is a list of rows, with each row being a list of cell values. + """ + return [ + ["Row 1", 1], + ["Row 2", 2], + ] From 3574d119465661ee4bde3552b43038359aa5524e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 9 Sep 2024 13:55:10 +0530 Subject: [PATCH 22/26] refactor: standard filters (cherry picked from commit dccbc1f432a83f07ef46582ac36df8fd776d4a4d) --- .../invalid_ledger_entries.js | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js index 548a6f7d951..ffaaf5d7cbf 100644 --- a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js @@ -1,13 +1,52 @@ // Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +function get_filters() { + let filters = [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + { + fieldname: "from_date", + label: __("Start Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + fieldname: "to_date", + label: __("End Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.get_today(), + }, + { + fieldname: "account", + label: __("Account"), + fieldtype: "MultiSelectList", + options: "Account", + get_data: function (txt) { + return frappe.db.get_link_options("Account", txt, { + company: frappe.query_report.get_filter_value("company"), + account_type: ["in", ["Receivable", "Payable"]], + }); + }, + }, + { + fieldname: "voucher_no", + label: __("Voucher No"), + fieldtype: "Data", + width: 100, + }, + ]; + return filters; +} + frappe.query_reports["Invalid Ledger Entries"] = { - filters: [ - // { - // "fieldname": "my_filter", - // "label": __("My Filter"), - // "fieldtype": "Data", - // "reqd": 1, - // }, - ], + filters: get_filters(), }; From d51cf281c310caeea61d88a66007f8d7e2d887a1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 9 Sep 2024 17:53:59 +0530 Subject: [PATCH 23/26] refactor: barebones methods with basic logic (cherry picked from commit b05b378ef0fcd4c08404fd541857921702751676) --- .../invalid_ledger_entries.py | 121 +++++++++++++++--- 1 file changed, 105 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py index 4f4b1835227..e1599a6758e 100644 --- a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py @@ -1,8 +1,10 @@ # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe -from frappe import _ +import frappe +from frappe import _, qb +from frappe.query_builder import Criterion +from frappe.query_builder.custom import ConstantColumn def execute(filters: dict | None = None): @@ -12,8 +14,10 @@ def execute(filters: dict | None = None): dictionary and should return columns and data. It is called by the framework every time the report is refreshed or a filter is updated. """ + validate_filters(filters) + columns = get_columns() - data = get_data() + data = get_data(filters) return columns, data @@ -24,25 +28,110 @@ def get_columns() -> list[dict]: One field definition per column, just like a DocType field definition. """ return [ + {"label": _("Voucher Type"), "fieldname": "voucher_type", "fieldtype": "Link", "options": "DocType"}, { - "label": _("Column 1"), - "fieldname": "column_1", - "fieldtype": "Data", - }, - { - "label": _("Column 2"), - "fieldname": "column_2", - "fieldtype": "Int", + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", }, ] -def get_data() -> list[list]: +def get_data(filters) -> list[list]: """Return data for the report. The report data is a list of rows, with each row being a list of cell values. """ - return [ - ["Row 1", 1], - ["Row 2", 2], - ] + active_vouchers = get_active_vouchers_for_period(filters) + invalid_vouchers = identify_cancelled_vouchers(active_vouchers) + + return invalid_vouchers + + +def identify_cancelled_vouchers(active_vouchers: list[dict] | list | None = None) -> list[dict]: + cancelled_vouchers = [] + if active_vouchers: + # Group by voucher types and use single query to identify cancelled vouchers + vtypes = set([x.voucher_type for x in active_vouchers]) + + for _t in vtypes: + _names = [x.voucher_no for x in active_vouchers if x.voucher_type == _t] + dt = qb.DocType(_t) + non_active_vouchers = ( + qb.from_(dt) + .select(ConstantColumn(_t).as_("doctype"), dt.name) + .where(dt.docstatus.ne(1) & dt.name.isin(_names)) + .run() + ) + if non_active_vouchers: + cancelled_vouchers.extend(non_active_vouchers) + return cancelled_vouchers + + +def validate_filters(filters: dict | None = None): + if not filters: + frappe.throw(_("Filters missing")) + + if not filters.company: + frappe.throw(_("Company is mandatory")) + + if filters.from_date > filters.to_date: + frappe.throw(_("Start Date should be lower than End Date")) + + +def build_query_filters(filters: dict | None = None) -> list: + qb_filters = [] + if filters: + if filters.account: + qb_filters.append(qb.Field("account").isin(filters.account)) + + if filters.voucher_no: + qb_filters.append(qb.Field("voucher_no").eq(filters.voucher_no)) + + return qb_filters + + +def get_active_vouchers_for_period(filters: dict | None = None) -> list[dict]: + uniq_vouchers = [] + + if filters: + gle = qb.DocType("GL Entry") + ple = qb.DocType("Payment Ledger Entry") + + qb_filters = build_query_filters(filters) + + gl_vouchers = ( + qb.from_(gle) + .select(gle.voucher_type) + .distinct() + .select(gle.voucher_no) + .distinct() + .where( + gle.is_cancelled.eq(0) + & gle.company.eq(filters.company) + & gle.posting_date[filters.from_date : filters.to_date] + ) + .where(Criterion.all(qb_filters)) + .run(as_dict=True) + ) + + pl_vouchers = ( + qb.from_(ple) + .select(ple.voucher_type) + .distinct() + .select(ple.voucher_no) + .distinct() + .where( + ple.delinked.eq(0) + & ple.company.eq(filters.company) + & ple.posting_date[filters.from_date : filters.to_date] + ) + .where(Criterion.all(qb_filters)) + .run(as_dict=True) + ) + + uniq_vouchers.extend(gl_vouchers) + uniq_vouchers.extend(pl_vouchers) + + return uniq_vouchers From b2d361b495f88d09a6875ebdef20060415d024ce Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Sep 2024 12:58:05 +0530 Subject: [PATCH 24/26] refactor: fetch as dictionary (cherry picked from commit 2126b10a92c38aa12a35bc78b577fc2212d5da6a) --- .../report/invalid_ledger_entries/invalid_ledger_entries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py index e1599a6758e..33fda705cf2 100644 --- a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py @@ -60,9 +60,9 @@ def identify_cancelled_vouchers(active_vouchers: list[dict] | list | None = None dt = qb.DocType(_t) non_active_vouchers = ( qb.from_(dt) - .select(ConstantColumn(_t).as_("doctype"), dt.name) + .select(ConstantColumn(_t).as_("voucher_type"), dt.name.as_("voucher_no")) .where(dt.docstatus.ne(1) & dt.name.isin(_names)) - .run() + .run(as_dict=True) ) if non_active_vouchers: cancelled_vouchers.extend(non_active_vouchers) From 2a890f9061f886844015fe251b829d0b9d0f2857 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Sep 2024 13:03:49 +0530 Subject: [PATCH 25/26] refactor: allow all accounts (cherry picked from commit 43198c946b498ab76ebbcd39de3d5cc031d897db) --- .../report/invalid_ledger_entries/invalid_ledger_entries.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js index ffaaf5d7cbf..47d478f2865 100644 --- a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js @@ -33,7 +33,6 @@ function get_filters() { get_data: function (txt) { return frappe.db.get_link_options("Account", txt, { company: frappe.query_report.get_filter_value("company"), - account_type: ["in", ["Receivable", "Payable"]], }); }, }, From 91105344bbe60e9a6d04153f74ca10a7b054cecb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:07:52 +0530 Subject: [PATCH 26/26] fix: bom cost update is not working (backport #43155) (#43156) fix: bom cost update is not working (#43155) (cherry picked from commit 05f9015c0bf341f25c340a69cfd95cebd7777115) Co-authored-by: rohitwaghchaure --- .../doctype/bom_update_log/bom_update_log.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 02c43a8beff..388094a9d87 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -8,7 +8,7 @@ from frappe import _ from frappe.model.document import Document from frappe.query_builder import DocType, Interval from frappe.query_builder.functions import Now -from frappe.utils import cint, cstr +from frappe.utils import cint, cstr, date_diff, today from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import ( get_leaf_boms, @@ -67,10 +67,12 @@ class BOMUpdateLog(Document): wip_log = frappe.get_all( "BOM Update Log", - {"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]}, + fields=["name", "modified"], + filters={"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]}, limit_page_length=1, ) - if wip_log: + + if wip_log and date_diff(today(), wip_log[0].modified) < 1: log_link = frappe.utils.get_link_to_form("BOM Update Log", wip_log[0].name) frappe.throw( _("BOM Updation already in progress. Please wait until {0} is complete.").format(log_link),