Merge pull request #34021 from ruthra-kumar/handle_rare_cases_of_null_in_outstanding_calculation
fix: rare instances of IntegrityError while cancelling journals against cr note
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 154fdc0..675a328 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -234,7 +234,7 @@
def allocate_entries(self, args):
self.validate_entries()
- invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"))
+ invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
default_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "exchange_gain_loss_account"
)
@@ -253,6 +253,9 @@
pay["amount"] = 0
inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number"))
+ if pay.get("reference_type") in ["Sales Invoice", "Purchase Invoice"]:
+ pay["exchange_rate"] = invoice_exchange_map.get(pay.get("reference_name"))
+
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
res.difference_account = default_exchange_gain_loss_account
res.exchange_rate = inv.get("exchange_rate")
@@ -407,13 +410,21 @@
if not self.get("payments"):
frappe.throw(_("No records found in the Payments table"))
- def get_invoice_exchange_map(self, invoices):
+ def get_invoice_exchange_map(self, invoices, payments):
sales_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice"
]
+
+ sales_invoices.extend(
+ [d.get("reference_name") for d in payments if d.get("reference_type") == "Sales Invoice"]
+ )
purchase_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice"
]
+ purchase_invoices.extend(
+ [d.get("reference_name") for d in payments if d.get("reference_type") == "Purchase Invoice"]
+ )
+
invoice_exchange_map = frappe._dict()
if sales_invoices:
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 00e3934..f9dda05 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -473,6 +473,11 @@
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}))
+
+ # Cr Note and Invoice are of the same currency. There shouldn't any difference amount.
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
pr.reconcile()
pr.get_unreconciled_entries()
@@ -506,6 +511,11 @@
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].allocated_amount = allocated_amount
+
+ # Cr Note and Invoice are of the same currency. There shouldn't any difference amount.
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
pr.reconcile()
# assert outstanding
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index 6b42e4d..b348bd3 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -85,11 +85,15 @@
}
if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
- this.frm.add_custom_button(
- __("Sales Order"),
- this.frm.cscript["Make Sales Order"],
- __("Create")
- );
+ if (frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation
+ || (!doc.valid_till)
+ || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
+ this.frm.add_custom_button(
+ __("Sales Order"),
+ this.frm.cscript["Make Sales Order"],
+ __("Create")
+ );
+ }
if(doc.status!=="Ordered") {
this.frm.add_custom_button(__('Set as Lost'), () => {
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 6836d56..063813b 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -195,6 +195,17 @@
@frappe.whitelist()
def make_sales_order(source_name: str, target_doc=None):
+ if not frappe.db.get_singles_value(
+ "Selling Settings", "allow_sales_order_creation_for_expired_quotation"
+ ):
+ quotation = frappe.db.get_value(
+ "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1
+ )
+ if quotation.valid_till and (
+ quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())
+ ):
+ frappe.throw(_("Validity period of this quotation has ended."))
+
return _make_sales_order(source_name, target_doc)
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 5aaba4f..cdf5f5d 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -144,11 +144,21 @@
def test_so_from_expired_quotation(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
+ frappe.db.set_single_value(
+ "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 0
+ )
+
quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(nowdate(), -1)
quotation.insert()
quotation.submit()
+ self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
+
+ frappe.db.set_single_value(
+ "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 1
+ )
+
make_sales_order(quotation.name)
def test_shopping_cart_without_website_item(self):
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 2abb169..6ea66a0 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -27,6 +27,7 @@
"column_break_5",
"allow_multiple_items",
"allow_against_multiple_purchase_orders",
+ "allow_sales_order_creation_for_expired_quotation",
"hide_tax_id",
"enable_discount_accounting"
],
@@ -172,6 +173,12 @@
"fieldname": "enable_discount_accounting",
"fieldtype": "Check",
"label": "Enable Discount Accounting for Selling"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_sales_order_creation_for_expired_quotation",
+ "fieldtype": "Check",
+ "label": "Allow Sales Order Creation For Expired Quotation"
}
],
"icon": "fa fa-cog",
@@ -179,7 +186,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2022-05-31 19:39:48.398738",
+ "modified": "2023-02-04 12:37:53.380857",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
index 991ac71..990d736 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
@@ -103,6 +103,11 @@
return options
}
},
+ {
+ "fieldname":"only_immediate_upcoming_term",
+ "label": __("Show only the Immediate Upcoming Term"),
+ "fieldtype": "Check",
+ },
]
return filters;
}
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
index 8bf5686..3682c5f 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
@@ -4,6 +4,7 @@
import frappe
from frappe import _, qb, query_builder
from frappe.query_builder import Criterion, functions
+from frappe.utils.dateutils import getdate
def get_columns():
@@ -208,6 +209,7 @@
)
.where(
(so.docstatus == 1)
+ & (so.status.isin(["To Deliver and Bill", "To Bill"]))
& (so.payment_terms_template != "NULL")
& (so.company == conditions.company)
& (so.transaction_date[conditions.start_date : conditions.end_date])
@@ -291,6 +293,18 @@
return sales_orders
+def filter_for_immediate_upcoming_term(filters, sales_orders):
+ if filters.only_immediate_upcoming_term and sales_orders:
+ immediate_term_found = set()
+ filtered_data = []
+ for order in sales_orders:
+ if order.name not in immediate_term_found and order.due_date > getdate():
+ filtered_data.append(order)
+ immediate_term_found.add(order.name)
+ return filtered_data
+ return sales_orders
+
+
def execute(filters=None):
columns = get_columns()
sales_orders, so_invoices = get_so_with_invoices(filters)
@@ -298,6 +312,8 @@
sales_orders = filter_on_calculated_status(filters, sales_orders)
+ sales_orders = filter_for_immediate_upcoming_term(filters, sales_orders)
+
prepare_chart(sales_orders)
data = sales_orders
diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py
index bb120ea..62936fc 100644
--- a/erpnext/startup/boot.py
+++ b/erpnext/startup/boot.py
@@ -25,6 +25,12 @@
frappe.db.get_single_value("CRM Settings", "default_valid_till")
)
+ bootinfo.sysdefaults.allow_sales_order_creation_for_expired_quotation = cint(
+ frappe.db.get_single_value(
+ "Selling Settings", "allow_sales_order_creation_for_expired_quotation"
+ )
+ )
+
# if no company, show a dialog box to create a new company
bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0]