diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
index d931f627dbd..ad68352c2a4 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
@@ -3,22 +3,36 @@
frappe.ui.form.on("Currency Exchange Settings", {
service_provider: function (frm) {
- if (frm.doc.service_provider == "exchangerate.host") {
- let result = ["result"];
- let params = {
- date: "{transaction_date}",
- from: "{from_currency}",
- to: "{to_currency}",
- };
- add_param(frm, "https://api.exchangerate.host/convert", params, result);
- } else if (frm.doc.service_provider == "frankfurter.app") {
- let result = ["rates", "{to_currency}"];
- let params = {
- base: "{from_currency}",
- symbols: "{to_currency}",
- };
- add_param(frm, "https://frankfurter.app/{transaction_date}", params, result);
- }
+ frm.call({
+ method: "erpnext.accounts.doctype.currency_exchange_settings.currency_exchange_settings.get_api_endpoint",
+ args: {
+ service_provider: frm.doc.service_provider,
+ use_http: frm.doc.use_http,
+ },
+ callback: function (r) {
+ if (r && r.message) {
+ if (frm.doc.service_provider == "exchangerate.host") {
+ let result = ["result"];
+ let params = {
+ date: "{transaction_date}",
+ from: "{from_currency}",
+ to: "{to_currency}",
+ };
+ add_param(frm, r.message, params, result);
+ } else if (frm.doc.service_provider == "frankfurter.app") {
+ let result = ["rates", "{to_currency}"];
+ let params = {
+ base: "{from_currency}",
+ symbols: "{to_currency}",
+ };
+ add_param(frm, r.message, params, result);
+ }
+ }
+ },
+ });
+ },
+ use_http: function (frm) {
+ frm.trigger("service_provider");
},
});
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 df232a5848c..bd90b8add80 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",
+ "use_http",
"access_key",
"url",
"column_break_3",
@@ -91,12 +92,19 @@
"fieldname": "access_key",
"fieldtype": "Data",
"label": "Access Key"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.service_provider != \"Custom\"",
+ "fieldname": "use_http",
+ "fieldtype": "Check",
+ "label": "Use HTTP Protocol"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-10-04 15:30:25.333860",
+ "modified": "2024-03-18 08:32:26.895076",
"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 117d5ff21e8..7a420f984ad 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
self.set("result_key", [])
self.set("req_params", [])
- self.api_endpoint = "https://api.exchangerate.host/convert"
+ self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
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"})
@@ -40,7 +40,7 @@ class CurrencyExchangeSettings(Document):
self.set("result_key", [])
self.set("req_params", [])
- self.api_endpoint = "https://frankfurter.app/{transaction_date}"
+ self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
self.append("result_key", {"key": "rates"})
self.append("result_key", {"key": "{to_currency}"})
self.append("req_params", {"key": "base", "value": "{from_currency}"})
@@ -79,3 +79,19 @@ class CurrencyExchangeSettings(Document):
frappe.throw(_("Returned exchange rate is neither integer not float."))
self.url = response.url
+
+
+@frappe.whitelist()
+def get_api_endpoint(service_provider: str = None, use_http: bool = False):
+ if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
+ if service_provider == "exchangerate.host":
+ api = "api.exchangerate.host/convert"
+ elif service_provider == "frankfurter.app":
+ api = "frankfurter.app/{transaction_date}"
+
+ protocol = "https://"
+ if use_http:
+ protocol = "http://"
+
+ return protocol + api
+ return None
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index 977cfe94f8a..00d62f3f601 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -606,21 +606,21 @@ def get_account_details(
if account_balance and (
account_balance[0].balance or account_balance[0].balance_in_account_currency
):
- account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
+ if account_with_new_balance := ExchangeRateRevaluation.calculate_new_account_balance(
company, posting_date, account_balance
- )
- row = account_with_new_balance[0]
- account_details.update(
- {
- "balance_in_base_currency": row["balance_in_base_currency"],
- "balance_in_account_currency": row["balance_in_account_currency"],
- "current_exchange_rate": row["current_exchange_rate"],
- "new_exchange_rate": row["new_exchange_rate"],
- "new_balance_in_base_currency": row["new_balance_in_base_currency"],
- "new_balance_in_account_currency": row["new_balance_in_account_currency"],
- "zero_balance": row["zero_balance"],
- "gain_loss": row["gain_loss"],
- }
- )
+ ):
+ row = account_with_new_balance[0]
+ account_details.update(
+ {
+ "balance_in_base_currency": row["balance_in_base_currency"],
+ "balance_in_account_currency": row["balance_in_account_currency"],
+ "current_exchange_rate": row["current_exchange_rate"],
+ "new_exchange_rate": row["new_exchange_rate"],
+ "new_balance_in_base_currency": row["new_balance_in_base_currency"],
+ "new_balance_in_account_currency": row["new_balance_in_account_currency"],
+ "zero_balance": row["zero_balance"],
+ "gain_loss": row["gain_loss"],
+ }
+ )
return account_details
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json
index 592eaecc1c5..eb99768b05e 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.json
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json
@@ -174,7 +174,8 @@
"fieldname": "voucher_detail_no",
"fieldtype": "Data",
"label": "Voucher Detail No",
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "project",
@@ -256,7 +257,8 @@
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
- "modified": "2020-04-07 16:22:33.766994",
+ "links": [],
+ "modified": "2024-03-19 18:30:49.613401",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GL Entry",
@@ -288,5 +290,6 @@
"quick_entry": 1,
"search_fields": "voucher_no,account,posting_date,against_voucher",
"sort_field": "modified",
- "sort_order": "DESC"
-}
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index d04f0ac7f3c..e0e8e2154a6 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -350,7 +350,9 @@ class PaymentEntry(AccountsController):
ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency
)
- if ref_exchange_rate:
+
+ # Only update exchange rate when the reference is Journal Entry
+ if ref_exchange_rate and d.reference_doctype == "Journal Entry":
ref_details.update({"exchange_rate": ref_exchange_rate})
for field, value in ref_details.items():
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index eb4396a5c6f..662077d027e 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -1130,6 +1130,17 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(pr.allocation[0].allocated_amount, 85)
self.assertEqual(pr.allocation[0].difference_amount, 0)
+ pr.reconcile()
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+ # No Exchange Gain/Loss journal should be generated
+ exc_gain_loss_journals = frappe.db.get_all(
+ "Journal Entry Account",
+ filters={"reference_type": si.doctype, "reference_name": si.name, "docstatus": 1},
+ fields=["parent"],
+ )
+ self.assertEqual(exc_gain_loss_journals, [])
+
def test_reconciliation_purchase_invoice_against_return(self):
self.supplier = "_Test Supplier USD"
pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True)
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
index 5307ccb1931..81ebf9744c4 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
@@ -89,10 +89,11 @@
- | 30 Days |
- 60 Days |
- 90 Days |
- 120 Days |
+ 0 - 30 Days |
+ 30 - 60 Days |
+ 60 - 90 Days |
+ 90 - 120 Days |
+ Above 120 Days |
@@ -101,6 +102,7 @@
{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }} |
{{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }} |
{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }} |
+ {{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }} |
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 4719876f4ec..f54787de717 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -737,7 +737,6 @@ class PurchaseInvoice(BuyingController):
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
)
)
- self.provisional_enpenses_booked_in_pr = False
if provisional_accounting_for_non_stock_items:
self.get_provisional_accounts()
@@ -982,37 +981,36 @@ class PurchaseInvoice(BuyingController):
fields=["name", "provisional_expense_account", "qty", "base_rate"],
)
default_provisional_account = self.get_company_default("default_provisional_account")
+ provisional_accounts = set(
+ [
+ d.provisional_expense_account if d.provisional_expense_account else default_provisional_account
+ for d in pr_items
+ ]
+ )
+
+ provisional_gl_entries = frappe.get_all(
+ "GL Entry",
+ filters={
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": ("in", linked_purchase_receipts),
+ "account": ("in", provisional_accounts),
+ "is_cancelled": 0,
+ },
+ fields=["voucher_detail_no"],
+ )
+ rows_with_provisional_entries = [d.voucher_detail_no for d in provisional_gl_entries]
for item in pr_items:
self.provisional_accounts[item.name] = {
"provisional_account": item.provisional_expense_account or default_provisional_account,
"qty": item.qty,
"base_rate": item.base_rate,
+ "has_provisional_entry": item.name in rows_with_provisional_entries,
}
def make_provisional_gl_entry(self, gl_entries, item):
if item.purchase_receipt:
- if not self.provisional_enpenses_booked_in_pr:
- pr_item = self.provisional_accounts.get(item.pr_detail, {})
- provisional_account = pr_item.get("provisional_account")
- pr_qty = pr_item.get("qty")
- pr_base_rate = pr_item.get("base_rate")
-
- # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
- provision_gle_against_pr = frappe.db.get_value(
- "GL Entry",
- {
- "is_cancelled": 0,
- "voucher_type": "Purchase Receipt",
- "voucher_no": item.purchase_receipt,
- "voucher_detail_no": item.pr_detail,
- "account": provisional_account,
- },
- ["name"],
- )
- if provision_gle_against_pr:
- self.provisional_enpenses_booked_in_pr = True
-
- if self.provisional_enpenses_booked_in_pr:
+ pr_item = self.provisional_accounts.get(item.pr_detail, {})
+ if pr_item.get("has_provisional_entry"):
purchase_receipt_doc = frappe.get_cached_doc("Purchase Receipt", item.purchase_receipt)
# Intentionally passing purchase invoice item to handle partial billing
@@ -1020,9 +1018,9 @@ class PurchaseInvoice(BuyingController):
item,
gl_entries,
self.posting_date,
- provisional_account,
+ pr_item.get("provisional_account"),
reverse=1,
- item_amount=(min(item.qty, pr_qty) * pr_base_rate),
+ item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")),
)
def update_gross_purchase_amount_for_linked_assets(self, item):
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 3d59d288e4d..cafdc0e12c6 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -740,6 +740,7 @@
"fieldtype": "Currency",
"label": "Landed Cost Voucher Amount",
"no_copy": 1,
+ "options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -893,7 +894,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-12-25 22:00:28.043555",
+ "modified": "2024-03-19 19:09:47.210965",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@@ -903,4 +904,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 1e8a912030a..865362fbb48 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -2169,7 +2169,8 @@
"description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
"fieldname": "update_outstanding_for_self",
"fieldtype": "Check",
- "label": "Update Outstanding for Self"
+ "label": "Update Outstanding for Self",
+ "no_copy": 1
}
],
"icon": "fa fa-file-text",
@@ -2182,7 +2183,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2024-03-11 14:20:34.874192",
+ "modified": "2024-03-15 16:44:17.778370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index ce5456da5d3..4d0f6446da4 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1569,6 +1569,12 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
+ def test_zero_qty_return_invoice_with_stock_effect(self):
+ cr_note = create_sales_invoice(qty=-1, rate=300, is_return=1, do_not_submit=True)
+ cr_note.update_stock = True
+ cr_note.items[0].qty = 0
+ self.assertRaises(frappe.ValidationError, cr_note.save)
+
def test_return_invoice_with_account_mismatch(self):
debtors2 = create_account(
parent_account="Accounts Receivable - _TC",
diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js
index c109abd8146..f7d0d947b61 100644
--- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js
+++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js
@@ -77,7 +77,10 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
fieldname: "group_by",
label: __("Group by"),
fieldtype: "Select",
- options: [__("Group by Supplier"), __("Group by Item")],
+ options: [
+ { label: __("Group by Supplier"), value: "Group by Supplier" },
+ { label: __("Group by Item"), value: "Group by Item" },
+ ],
default: __("Group by Supplier"),
},
{
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 8055b8180de..61bdc4f52a9 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -157,6 +157,13 @@ class AccountsController(TransactionBase):
if not self.get("is_return") and not self.get("is_debit_note"):
self.validate_qty_is_not_zero()
+ if (
+ self.doctype in ["Sales Invoice", "Purchase Invoice"]
+ and self.get("is_return")
+ and self.get("update_stock")
+ ):
+ self.validate_zero_qty_for_return_invoices_with_stock()
+
if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True)
@@ -950,6 +957,18 @@ class AccountsController(TransactionBase):
return gl_dict
+ def validate_zero_qty_for_return_invoices_with_stock(self):
+ rows = []
+ for item in self.items:
+ if not flt(item.qty):
+ rows.append(item)
+ if rows:
+ frappe.throw(
+ _(
+ "For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}"
+ ).format(frappe.bold(comma_and(["#" + str(x.idx) for x in rows])))
+ )
+
def validate_qty_is_not_zero(self):
if self.doctype == "Purchase Receipt":
return
diff --git a/erpnext/e_commerce/doctype/item_review/test_item_review.py b/erpnext/e_commerce/doctype/item_review/test_item_review.py
index 8a4befc800a..6147a9153e6 100644
--- a/erpnext/e_commerce/doctype/item_review/test_item_review.py
+++ b/erpnext/e_commerce/doctype/item_review/test_item_review.py
@@ -5,6 +5,7 @@ import unittest
import frappe
from frappe.core.doctype.user_permission.test_user_permission import create_user
+from frappe.tests.utils import FrappeTestCase
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
setup_e_commerce_settings,
@@ -19,7 +20,7 @@ from erpnext.e_commerce.shopping_cart.cart import get_party
from erpnext.stock.doctype.item.test_item import make_item
-class TestItemReview(unittest.TestCase):
+class TestItemReview(FrappeTestCase):
def setUp(self):
item = make_item("Test Mobile Phone")
if not frappe.db.exists("Website Item", {"item_code": "Test Mobile Phone"}):
@@ -29,8 +30,7 @@ class TestItemReview(unittest.TestCase):
frappe.local.shopping_cart_settings = None
def tearDown(self):
- frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
- setup_e_commerce_settings({"enable_reviews": 0})
+ frappe.db.rollback()
def test_add_and_get_item_reviews_from_customer(self):
"Add / Get Reviews from a User that is a valid customer (has added to cart or purchased in the past)"
@@ -44,7 +44,7 @@ class TestItemReview(unittest.TestCase):
# post review on "Test Mobile Phone"
try:
- add_item_review(web_item, "Great Product", 3, "Would recommend this product")
+ add_item_review(web_item, "Great Product", 1, "Would recommend this product")
review_name = frappe.db.get_value("Item Review", {"website_item": web_item})
except Exception:
self.fail(f"Error while publishing review for {web_item}")
@@ -52,8 +52,7 @@ class TestItemReview(unittest.TestCase):
review_data = get_item_reviews(web_item, 0, 10)
self.assertEqual(len(review_data.reviews), 1)
- self.assertEqual(review_data.average_rating, 3)
- self.assertEqual(review_data.reviews_per_rating[2], 100)
+ self.assertEqual(review_data.average_rating, 1)
# tear down
frappe.set_user("Administrator")
diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py
index 8eebfdb83af..8dffb4364e1 100644
--- a/erpnext/e_commerce/doctype/website_item/test_website_item.py
+++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py
@@ -24,10 +24,11 @@ WEBITEM_PRICE_TESTS = (
"test_website_item_price_for_guest_user",
)
+from frappe.tests.utils import FrappeTestCase
-class TestWebsiteItem(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
+
+class TestWebsiteItem(FrappeTestCase):
+ def setUp(self):
setup_e_commerce_settings(
{
"company": "_Test Company",
@@ -37,11 +38,6 @@ class TestWebsiteItem(unittest.TestCase):
}
)
- @classmethod
- def tearDownClass(cls):
- frappe.db.rollback()
-
- def setUp(self):
if self._testMethodName in WEBITEM_DESK_TESTS:
make_item(
"Test Web Item",
@@ -75,6 +71,9 @@ class TestWebsiteItem(unittest.TestCase):
customer="_Test Customer",
)
+ def tearDown(self):
+ frappe.db.rollback()
+
def test_index_creation(self):
"Check if index is getting created in db."
from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 2de0168812d..ee7cb323285 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -2160,6 +2160,40 @@ class TestSalesOrder(FrappeTestCase):
self.assertFalse(row.warehouse == rejected_warehouse)
self.assertTrue(row.warehouse == warehouse)
+ def test_auto_update_price_list(self):
+ item = make_item(
+ "_Test Auto Update Price List Item",
+ )
+
+ frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
+ so = make_sales_order(
+ item_code=item.name, currency="USD", qty=1, rate=100, price_list_rate=100, do_not_submit=True
+ )
+ so.save()
+
+ item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
+ self.assertEqual(item_price, 100)
+
+ so = make_sales_order(
+ item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=100, do_not_submit=True
+ )
+ so.save()
+
+ item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
+ self.assertEqual(item_price, 100)
+
+ frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 1)
+ so = make_sales_order(
+ item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=200, do_not_submit=True
+ )
+ so.save()
+
+ item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
+ self.assertEqual(item_price, 200)
+
+ frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
+ frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
+
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
@@ -2225,13 +2259,14 @@ def make_sales_order(**args):
return so
-def create_dn_against_so(so, delivered_qty=0):
- frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
+def create_dn_against_so(so, delivered_qty=0, do_not_submit=False):
+ frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
dn = make_delivery_note(so)
dn.get("items")[0].qty = delivered_qty or 5
dn.insert()
- dn.submit()
+ if not do_not_submit:
+ dn.submit()
return dn
diff --git a/erpnext/setup/demo_data/item.json b/erpnext/setup/demo_data/item.json
new file mode 100644
index 00000000000..17024341225
--- /dev/null
+++ b/erpnext/setup/demo_data/item.json
@@ -0,0 +1,92 @@
+[
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU001",
+ "item_name": "T-shirt",
+ "valuation_rate": 400.0,
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/1484808/pexels-photo-1484808.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU002",
+ "valuation_rate": 300.0,
+ "item_name": "Laptop",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/3999538/pexels-photo-3999538.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU003",
+ "valuation_rate": 523.0,
+ "item_name": "Book",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/2422178/pexels-photo-2422178.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU004",
+ "valuation_rate": 725.0,
+ "item_name": "Smartphone",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/1647976/pexels-photo-1647976.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU005",
+ "valuation_rate": 222.0,
+ "item_name": "Sneakers",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/1598505/pexels-photo-1598505.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU006",
+ "valuation_rate": 420.0,
+ "item_name": "Coffee Mug",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/585753/pexels-photo-585753.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU007",
+ "valuation_rate": 375.0,
+ "item_name": "Television",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/8059376/pexels-photo-8059376.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU008",
+ "valuation_rate": 333.0,
+ "item_name": "Backpack",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/3731256/pexels-photo-3731256.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU009",
+ "valuation_rate": 700.0,
+ "item_name": "Headphones",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/3587478/pexels-photo-3587478.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU010",
+ "valuation_rate": 500.0,
+ "item_name": "Camera",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/51383/photo-camera-subject-photographer-51383.jpeg"
+ }
+]
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 4216ca0cdc3..a402bb5aed2 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -130,6 +130,7 @@ class DeliveryNote(SellingController):
def validate(self):
self.validate_posting_time()
super(DeliveryNote, self).validate()
+ self.validate_references()
self.set_status()
self.so_required()
self.validate_proj_cust()
@@ -195,6 +196,58 @@ class DeliveryNote(SellingController):
]
)
+ def validate_references(self):
+ self.validate_sales_order_references()
+ self.validate_sales_invoice_references()
+
+ def validate_sales_order_references(self):
+ err_msg = ""
+ 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")
+ )
+ + "
"
+ )
+
+ if err_msg:
+ frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete"))
+
+ def validate_sales_invoice_references(self):
+ err_msg = ""
+ 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")
+ )
+ + "
"
+ )
+
+ if err_msg:
+ frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete"))
+
def validate_proj_cust(self):
"""check for does customer belong to same project as entered.."""
if self.project and self.customer:
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 9f2c0be75d0..6e735ea67b7 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -723,6 +723,15 @@ class TestDeliveryNote(FrappeTestCase):
dn.cancel()
self.assertEqual(dn.status, "Cancelled")
+ def test_sales_order_reference_validation(self):
+ so = make_sales_order(po_no="12345")
+ dn = create_dn_against_so(so.name, delivered_qty=2, do_not_submit=True)
+ dn.items[0].against_sales_order = None
+ self.assertRaises(frappe.ValidationError, dn.save)
+ dn.reload()
+ dn.items[0].so_detail = None
+ self.assertRaises(frappe.ValidationError, dn.save)
+
def test_dn_billing_status_case1(self):
# SO -> DN -> SI
so = make_sales_order(po_no="12345")
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 831fcac93ce..289a654ac12 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -854,7 +854,9 @@ def get_price_list_rate(args, item_doc, out=None):
price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
# insert in database
- if price_list_rate is None:
+ if price_list_rate is None or frappe.db.get_single_value(
+ "Stock Settings", "update_existing_price_list_rate"
+ ):
if args.price_list and args.rate:
insert_item_price(args)
return out