Merge pull request #40537 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
92
erpnext/setup/demo_data/item.json
Normal file
92
erpnext/setup/demo_data/item.json
Normal 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"
|
||||
}
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user