Merge pull request #37395 from ruthra-kumar/exception_on_exporting_errored_rows_in_bank_statement_import
fix: exception on exporting errored rows
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
index c62b711..df232a5 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
@@ -9,6 +9,7 @@
"disabled",
"service_provider",
"api_endpoint",
+ "access_key",
"url",
"column_break_3",
"help",
@@ -84,12 +85,18 @@
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
+ },
+ {
+ "depends_on": "eval:doc.service_provider == 'exchangerate.host';",
+ "fieldname": "access_key",
+ "fieldtype": "Data",
+ "label": "Access Key"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-01-09 12:19:03.955906",
+ "modified": "2023-10-04 15:30:25.333860",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
index d618c5c..117d5ff 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
@@ -18,11 +18,21 @@
def set_parameters_and_result(self):
if self.service_provider == "exchangerate.host":
+
+ if not self.access_key:
+ frappe.throw(
+ _("Access Key is required for Service Provider: {0}").format(
+ frappe.bold(self.service_provider)
+ )
+ )
+
self.set("result_key", [])
self.set("req_params", [])
self.api_endpoint = "https://api.exchangerate.host/convert"
self.append("result_key", {"key": "result"})
+ self.append("req_params", {"key": "access_key", "value": self.access_key})
+ self.append("req_params", {"key": "amount", "value": "1"})
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"})
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index cdd1203..22b6880 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -53,7 +53,15 @@
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
},
-
+ before_save: function(frm) {
+ if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) {
+ let payment_entry_references = frm.doc.accounts.filter(elem => (elem.reference_type == "Payment Entry"));
+ if (payment_entry_references.length > 0) {
+ let rows = payment_entry_references.map(x => "#"+x.idx);
+ frappe.throw(__("Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.", [frappe.utils.comma_and(rows)]));
+ }
+ }
+ },
make_inter_company_journal_entry: function(frm) {
var d = new frappe.ui.Dialog({
title: __("Select Company"),
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 52ae951..6c959ba 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -48,6 +48,20 @@
def get_report_pdf(doc, consolidated=True):
+ statement_dict = get_statement_dict(doc)
+ if not bool(statement_dict):
+ return False
+ elif consolidated:
+ delimiter = '<div style="page-break-before: always;"></div>' if doc.include_break else ""
+ result = delimiter.join(list(statement_dict.values()))
+ return get_pdf(result, {"orientation": doc.orientation})
+ else:
+ for customer, statement_html in statement_dict.items():
+ statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
+ return statement_dict
+
+
+def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {}
ageing = ""
@@ -78,18 +92,11 @@
if not res:
continue
- statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
+ statement_dict[entry.customer] = (
+ [res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing)
+ )
- if not bool(statement_dict):
- return False
- elif consolidated:
- delimiter = '<div style="page-break-before: always;"></div>' if doc.include_break else ""
- result = delimiter.join(list(statement_dict.values()))
- return get_pdf(result, {"orientation": doc.orientation})
- else:
- for customer, statement_html in statement_dict.items():
- statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
- return statement_dict
+ return statement_dict
def set_ageing(doc, entry):
@@ -102,7 +109,8 @@
"range2": 60,
"range3": 90,
"range4": 120,
- "customer": entry.customer,
+ "party_type": "Customer",
+ "party": [entry.customer],
}
)
col1, ageing = get_ageing(ageing_filters)
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py
index fb0d8d1..a3a74df 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py
@@ -4,39 +4,107 @@
import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate, today
from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import (
+ get_statement_dict,
send_emails,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
-class TestProcessStatementOfAccounts(unittest.TestCase):
+class TestProcessStatementOfAccounts(AccountsTestMixin, FrappeTestCase):
def setUp(self):
+ self.create_company()
+ self.create_customer()
+ self.create_customer(customer_name="Other Customer")
+ self.clear_old_entries()
self.si = create_sales_invoice()
- self.process_soa = create_process_soa()
+ create_sales_invoice(customer="Other Customer")
+
+ def test_process_soa_for_gl(self):
+ """Tests the utils for Statement of Accounts(General Ledger)"""
+ process_soa = create_process_soa(
+ name="_Test Process SOA for GL",
+ customers=[{"customer": "_Test Customer"}, {"customer": "Other Customer"}],
+ )
+ statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
+
+ # Checks if the statements are filtered based on the Customer
+ self.assertIn("Other Customer", statement_dict)
+ self.assertIn("_Test Customer", statement_dict)
+
+ # Checks if the correct number of receivable entries exist
+ # 3 rows for opening and closing and 1 row for SI
+ receivable_entries = statement_dict["_Test Customer"][0]
+ self.assertEqual(len(receivable_entries), 4)
+
+ # Checks the amount for the receivable entry
+ self.assertEqual(receivable_entries[1].voucher_no, self.si.name)
+ self.assertEqual(receivable_entries[1].balance, 100)
+
+ def test_process_soa_for_ar(self):
+ """Tests the utils for Statement of Accounts(Accounts Receivable)"""
+ process_soa = create_process_soa(name="_Test Process SOA for AR", report="Accounts Receivable")
+ statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
+
+ # Checks if the statements are filtered based on the Customer
+ self.assertNotIn("Other Customer", statement_dict)
+ self.assertIn("_Test Customer", statement_dict)
+
+ # Checks if the correct number of receivable entries exist
+ receivable_entries = statement_dict["_Test Customer"][0]
+ self.assertEqual(len(receivable_entries), 1)
+
+ # Checks the amount for the receivable entry
+ self.assertEqual(receivable_entries[0].voucher_no, self.si.name)
+ self.assertEqual(receivable_entries[0].total_due, 100)
+
+ # Checks the ageing summary for AR
+ ageing_summary = statement_dict["_Test Customer"][1][0]
+ expected_summary = frappe._dict(
+ range1=100,
+ range2=0,
+ range3=0,
+ range4=0,
+ range5=0,
+ )
+ self.check_ageing_summary(ageing_summary, expected_summary)
def test_auto_email_for_process_soa_ar(self):
- send_emails(self.process_soa.name, from_scheduler=True)
- self.process_soa.load_from_db()
- self.assertEqual(self.process_soa.posting_date, getdate(add_days(today(), 7)))
+ process_soa = create_process_soa(
+ name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable"
+ )
+ send_emails(process_soa.name, from_scheduler=True)
+ process_soa.load_from_db()
+ self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7)))
+
+ def check_ageing_summary(self, ageing, expected_ageing):
+ for age_range in expected_ageing:
+ self.assertEqual(expected_ageing[age_range], ageing.get(age_range))
def tearDown(self):
- frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
+ frappe.db.rollback()
-def create_process_soa():
- frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
+def create_process_soa(**args):
+ args = frappe._dict(args)
+ frappe.delete_doc_if_exists("Process Statement Of Accounts", args.name)
process_soa = frappe.new_doc("Process Statement Of Accounts")
- soa_dict = {
- "name": "Test Process SOA",
- "company": "_Test Company",
- }
+ soa_dict = frappe._dict(
+ name=args.name,
+ company=args.company or "_Test Company",
+ customers=args.customers or [{"customer": "_Test Customer"}],
+ enable_auto_email=1 if args.enable_auto_email else 0,
+ frequency=args.frequency or "Weekly",
+ report=args.report or "General Ledger",
+ from_date=args.from_date or getdate(today()),
+ to_date=args.to_date or getdate(today()),
+ posting_date=args.posting_date or getdate(today()),
+ include_ageing=1,
+ )
process_soa.update(soa_dict)
- process_soa.set("customers", [{"customer": "_Test Customer"}])
- process_soa.enable_auto_email = 1
- process_soa.frequency = "Weekly"
- process_soa.report = "Accounts Receivable"
process_soa.save()
return process_soa
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 9014662..418a56f 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -288,7 +288,9 @@
last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1)
if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
- throw_message(item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate")
+ throw_message(
+ item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate (Moving Average)"
+ )
def get_item_list(self):
il = []
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 62d4c53..95bf0e4 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -190,7 +190,9 @@
item.net_rate = item.rate
- if not item.qty and self.doc.get("is_return"):
+ if (
+ not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt"
+ ):
item.amount = flt(-1 * item.rate, item.precision("amount"))
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index 105c58d..e897ba4 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -379,7 +379,7 @@
}
)
- set_address_details(out, lead, "Lead")
+ set_address_details(out, lead, "Lead", company=company)
taxes_and_charges = set_taxes(
None,
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index eeb09cb..70b70c3 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -135,7 +135,15 @@
}
else {
// allow for '0' qty on Credit/Debit notes
- let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1);
+ let qty = flt(item.qty);
+ if (!qty) {
+ qty = (me.frm.doc.is_debit_note ? 1 : -1);
+ if (me.frm.doc.doctype !== "Purchase Receipt" && me.frm.doc.is_return === 1) {
+ // In case of Purchase Receipt, qty can be 0 if all items are rejected
+ qty = flt(item.qty);
+ }
+ }
+
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
}
diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
index 3b48c2b..8477984 100644
--- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
+++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
@@ -121,6 +121,7 @@
# Update Currency Exchange Rate
settings = frappe.get_single("Currency Exchange Settings")
settings.service_provider = "exchangerate.host"
+ settings.access_key = "12345667890"
settings.save()
# Update exchange
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 04eff54..6afa86e 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -956,6 +956,10 @@
total_amount += total_billable_amount
total_billed_amount += flt(item.billed_amt)
+
+ if pr_doc.get("is_return") and not total_amount and total_billed_amount:
+ total_amount = total_billed_amount
+
if adjust_incoming_rate:
adjusted_amt = 0.0
if item.billed_amt and item.amount:
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index b7712ee..a8ef5e8 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -2060,6 +2060,32 @@
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
+ def test_purchase_return_status_with_debit_note(self):
+ pr = make_purchase_receipt(rejected_qty=10, received_qty=10, rate=100, do_not_save=1)
+ pr.items[0].qty = 0
+ pr.items[0].stock_qty = 0
+ pr.submit()
+
+ return_pr = make_purchase_receipt(
+ is_return=1,
+ return_against=pr.name,
+ qty=0,
+ rejected_qty=10 * -1,
+ received_qty=10 * -1,
+ do_not_save=1,
+ )
+ return_pr.items[0].qty = 0.0
+ return_pr.items[0].stock_qty = 0.0
+ return_pr.submit()
+
+ self.assertEqual(return_pr.status, "To Bill")
+
+ pi = make_purchase_invoice(return_pr.name)
+ pi.submit()
+
+ return_pr.reload()
+ self.assertEqual(return_pr.status, "Completed")
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier