Merge pull request #40537 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
rohitwaghchaure
2024-03-20 10:52:20 +05:30
committed by GitHub
21 changed files with 364 additions and 91 deletions

View File

@@ -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");
},
});

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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": []
}

View File

@@ -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():

View File

@@ -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)

View File

@@ -89,10 +89,11 @@
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 25%">30 Days</th>
<th style="width: 25%">60 Days</th>
<th style="width: 25%">90 Days</th>
<th style="width: 25%">120 Days</th>
<th style="width: 20%">0 - 30 Days</th>
<th style="width: 20%">30 - 60 Days</th>
<th style="width: 20%">60 - 90 Days</th>
<th style="width: 20%">90 - 120 Days</th>
<th style="width: 20%">Above 120 Days</th>
</tr>
</thead>
<tbody>
@@ -101,6 +102,7 @@
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
</tr>
</tbody>
</table>

View File

@@ -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):

View File

@@ -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": []
}
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"),
},
{

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}
]

View File

@@ -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")
)
+ "<br>"
)
else:
err_msg += (
_("'Sales Order Item' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("so_detail")
)
+ "<br>"
)
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")
)
+ "<br>"
)
else:
err_msg += (
_("'Sales Invoice Item' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("si_detail")
)
+ "<br>"
)
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:

View File

@@ -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")

View File

@@ -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