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 @@ - - - - + + + + + @@ -101,6 +102,7 @@ +
30 Days60 Days90 Days120 Days0 - 30 Days30 - 60 Days60 - 90 Days90 - 120 DaysAbove 120 Days
{{ 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