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", { frappe.ui.form.on("Currency Exchange Settings", {
service_provider: function (frm) { service_provider: function (frm) {
if (frm.doc.service_provider == "exchangerate.host") { frm.call({
let result = ["result"]; method: "erpnext.accounts.doctype.currency_exchange_settings.currency_exchange_settings.get_api_endpoint",
let params = { args: {
date: "{transaction_date}", service_provider: frm.doc.service_provider,
from: "{from_currency}", use_http: frm.doc.use_http,
to: "{to_currency}", },
}; callback: function (r) {
add_param(frm, "https://api.exchangerate.host/convert", params, result); if (r && r.message) {
} else if (frm.doc.service_provider == "frankfurter.app") { if (frm.doc.service_provider == "exchangerate.host") {
let result = ["rates", "{to_currency}"]; let result = ["result"];
let params = { let params = {
base: "{from_currency}", date: "{transaction_date}",
symbols: "{to_currency}", from: "{from_currency}",
}; to: "{to_currency}",
add_param(frm, "https://frankfurter.app/{transaction_date}", params, result); };
} 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", "disabled",
"service_provider", "service_provider",
"api_endpoint", "api_endpoint",
"use_http",
"access_key", "access_key",
"url", "url",
"column_break_3", "column_break_3",
@@ -91,12 +92,19 @@
"fieldname": "access_key", "fieldname": "access_key",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Access Key" "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, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-10-04 15:30:25.333860", "modified": "2024-03-18 08:32:26.895076",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Currency Exchange Settings", "name": "Currency Exchange Settings",

View File

@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
self.set("result_key", []) self.set("result_key", [])
self.set("req_params", []) 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("result_key", {"key": "result"})
self.append("req_params", {"key": "access_key", "value": self.access_key}) self.append("req_params", {"key": "access_key", "value": self.access_key})
self.append("req_params", {"key": "amount", "value": "1"}) self.append("req_params", {"key": "amount", "value": "1"})
@@ -40,7 +40,7 @@ class CurrencyExchangeSettings(Document):
self.set("result_key", []) self.set("result_key", [])
self.set("req_params", []) 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": "rates"})
self.append("result_key", {"key": "{to_currency}"}) self.append("result_key", {"key": "{to_currency}"})
self.append("req_params", {"key": "base", "value": "{from_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.")) frappe.throw(_("Returned exchange rate is neither integer not float."))
self.url = response.url 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 ( if account_balance and (
account_balance[0].balance or account_balance[0].balance_in_account_currency 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 company, posting_date, account_balance
) ):
row = account_with_new_balance[0] row = account_with_new_balance[0]
account_details.update( account_details.update(
{ {
"balance_in_base_currency": row["balance_in_base_currency"], "balance_in_base_currency": row["balance_in_base_currency"],
"balance_in_account_currency": row["balance_in_account_currency"], "balance_in_account_currency": row["balance_in_account_currency"],
"current_exchange_rate": row["current_exchange_rate"], "current_exchange_rate": row["current_exchange_rate"],
"new_exchange_rate": row["new_exchange_rate"], "new_exchange_rate": row["new_exchange_rate"],
"new_balance_in_base_currency": row["new_balance_in_base_currency"], "new_balance_in_base_currency": row["new_balance_in_base_currency"],
"new_balance_in_account_currency": row["new_balance_in_account_currency"], "new_balance_in_account_currency": row["new_balance_in_account_currency"],
"zero_balance": row["zero_balance"], "zero_balance": row["zero_balance"],
"gain_loss": row["gain_loss"], "gain_loss": row["gain_loss"],
} }
) )
return account_details return account_details

View File

@@ -174,7 +174,8 @@
"fieldname": "voucher_detail_no", "fieldname": "voucher_detail_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Voucher Detail No", "label": "Voucher Detail No",
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "project", "fieldname": "project",
@@ -256,7 +257,8 @@
"icon": "fa fa-list", "icon": "fa fa-list",
"idx": 1, "idx": 1,
"in_create": 1, "in_create": 1,
"modified": "2020-04-07 16:22:33.766994", "links": [],
"modified": "2024-03-19 18:30:49.613401",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "GL Entry", "name": "GL Entry",
@@ -288,5 +290,6 @@
"quick_entry": 1, "quick_entry": 1,
"search_fields": "voucher_no,account,posting_date,against_voucher", "search_fields": "voucher_no,account,posting_date,against_voucher",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@@ -350,7 +350,9 @@ class PaymentEntry(AccountsController):
ref_details = get_reference_details( ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency 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}) ref_details.update({"exchange_rate": ref_exchange_rate})
for field, value in ref_details.items(): 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].allocated_amount, 85)
self.assertEqual(pr.allocation[0].difference_amount, 0) 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): def test_reconciliation_purchase_invoice_against_return(self):
self.supplier = "_Test Supplier USD" self.supplier = "_Test Supplier USD"
pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True) pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True)

View File

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

View File

@@ -737,7 +737,6 @@ class PurchaseInvoice(BuyingController):
"Company", self.company, "enable_provisional_accounting_for_non_stock_items" "Company", self.company, "enable_provisional_accounting_for_non_stock_items"
) )
) )
self.provisional_enpenses_booked_in_pr = False
if provisional_accounting_for_non_stock_items: if provisional_accounting_for_non_stock_items:
self.get_provisional_accounts() self.get_provisional_accounts()
@@ -982,37 +981,36 @@ class PurchaseInvoice(BuyingController):
fields=["name", "provisional_expense_account", "qty", "base_rate"], fields=["name", "provisional_expense_account", "qty", "base_rate"],
) )
default_provisional_account = self.get_company_default("default_provisional_account") 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: for item in pr_items:
self.provisional_accounts[item.name] = { self.provisional_accounts[item.name] = {
"provisional_account": item.provisional_expense_account or default_provisional_account, "provisional_account": item.provisional_expense_account or default_provisional_account,
"qty": item.qty, "qty": item.qty,
"base_rate": item.base_rate, "base_rate": item.base_rate,
"has_provisional_entry": item.name in rows_with_provisional_entries,
} }
def make_provisional_gl_entry(self, gl_entries, item): def make_provisional_gl_entry(self, gl_entries, item):
if item.purchase_receipt: if item.purchase_receipt:
if not self.provisional_enpenses_booked_in_pr: pr_item = self.provisional_accounts.get(item.pr_detail, {})
pr_item = self.provisional_accounts.get(item.pr_detail, {}) if pr_item.get("has_provisional_entry"):
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:
purchase_receipt_doc = frappe.get_cached_doc("Purchase Receipt", item.purchase_receipt) purchase_receipt_doc = frappe.get_cached_doc("Purchase Receipt", item.purchase_receipt)
# Intentionally passing purchase invoice item to handle partial billing # Intentionally passing purchase invoice item to handle partial billing
@@ -1020,9 +1018,9 @@ class PurchaseInvoice(BuyingController):
item, item,
gl_entries, gl_entries,
self.posting_date, self.posting_date,
provisional_account, pr_item.get("provisional_account"),
reverse=1, 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): def update_gross_purchase_amount_for_linked_assets(self, item):

View File

@@ -740,6 +740,7 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Landed Cost Voucher Amount", "label": "Landed Cost Voucher Amount",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -893,7 +894,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-12-25 22:00:28.043555", "modified": "2024-03-19 19:09:47.210965",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -2169,7 +2169,8 @@
"description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.", "description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
"fieldname": "update_outstanding_for_self", "fieldname": "update_outstanding_for_self",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Update Outstanding for Self" "label": "Update Outstanding for Self",
"no_copy": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@@ -2182,7 +2183,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2024-03-11 14:20:34.874192", "modified": "2024-03-15 16:44:17.778370",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "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", si1.name, "outstanding_amount"), -1000)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500) 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): def test_return_invoice_with_account_mismatch(self):
debtors2 = create_account( debtors2 = create_account(
parent_account="Accounts Receivable - _TC", parent_account="Accounts Receivable - _TC",

View File

@@ -77,7 +77,10 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
fieldname: "group_by", fieldname: "group_by",
label: __("Group by"), label: __("Group by"),
fieldtype: "Select", 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"), 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"): if not self.get("is_return") and not self.get("is_debit_note"):
self.validate_qty_is_not_zero() 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": if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True) self.set_missing_values(for_validate=True)
@@ -950,6 +957,18 @@ class AccountsController(TransactionBase):
return gl_dict 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): def validate_qty_is_not_zero(self):
if self.doctype == "Purchase Receipt": if self.doctype == "Purchase Receipt":
return return

View File

@@ -5,6 +5,7 @@ import unittest
import frappe import frappe
from frappe.core.doctype.user_permission.test_user_permission import create_user 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 ( from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
setup_e_commerce_settings, 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 from erpnext.stock.doctype.item.test_item import make_item
class TestItemReview(unittest.TestCase): class TestItemReview(FrappeTestCase):
def setUp(self): def setUp(self):
item = make_item("Test Mobile Phone") item = make_item("Test Mobile Phone")
if not frappe.db.exists("Website Item", {"item_code": "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 frappe.local.shopping_cart_settings = None
def tearDown(self): def tearDown(self):
frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete() frappe.db.rollback()
setup_e_commerce_settings({"enable_reviews": 0})
def test_add_and_get_item_reviews_from_customer(self): 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)" "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" # post review on "Test Mobile Phone"
try: 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}) review_name = frappe.db.get_value("Item Review", {"website_item": web_item})
except Exception: except Exception:
self.fail(f"Error while publishing review for {web_item}") 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) review_data = get_item_reviews(web_item, 0, 10)
self.assertEqual(len(review_data.reviews), 1) self.assertEqual(len(review_data.reviews), 1)
self.assertEqual(review_data.average_rating, 3) self.assertEqual(review_data.average_rating, 1)
self.assertEqual(review_data.reviews_per_rating[2], 100)
# tear down # tear down
frappe.set_user("Administrator") frappe.set_user("Administrator")

View File

@@ -24,10 +24,11 @@ WEBITEM_PRICE_TESTS = (
"test_website_item_price_for_guest_user", "test_website_item_price_for_guest_user",
) )
from frappe.tests.utils import FrappeTestCase
class TestWebsiteItem(unittest.TestCase):
@classmethod class TestWebsiteItem(FrappeTestCase):
def setUpClass(cls): def setUp(self):
setup_e_commerce_settings( setup_e_commerce_settings(
{ {
"company": "_Test Company", "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: if self._testMethodName in WEBITEM_DESK_TESTS:
make_item( make_item(
"Test Web Item", "Test Web Item",
@@ -75,6 +71,9 @@ class TestWebsiteItem(unittest.TestCase):
customer="_Test Customer", customer="_Test Customer",
) )
def tearDown(self):
frappe.db.rollback()
def test_index_creation(self): def test_index_creation(self):
"Check if index is getting created in db." "Check if index is getting created in db."
from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update 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.assertFalse(row.warehouse == rejected_warehouse)
self.assertTrue(row.warehouse == 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): def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings = frappe.get_doc("Accounts Settings")
@@ -2225,13 +2259,14 @@ def make_sales_order(**args):
return so return so
def create_dn_against_so(so, delivered_qty=0): def create_dn_against_so(so, delivered_qty=0, do_not_submit=False):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
dn = make_delivery_note(so) dn = make_delivery_note(so)
dn.get("items")[0].qty = delivered_qty or 5 dn.get("items")[0].qty = delivered_qty or 5
dn.insert() dn.insert()
dn.submit() if not do_not_submit:
dn.submit()
return dn 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): def validate(self):
self.validate_posting_time() self.validate_posting_time()
super(DeliveryNote, self).validate() super(DeliveryNote, self).validate()
self.validate_references()
self.set_status() self.set_status()
self.so_required() self.so_required()
self.validate_proj_cust() 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): def validate_proj_cust(self):
"""check for does customer belong to same project as entered..""" """check for does customer belong to same project as entered.."""
if self.project and self.customer: if self.project and self.customer:

View File

@@ -723,6 +723,15 @@ class TestDeliveryNote(FrappeTestCase):
dn.cancel() dn.cancel()
self.assertEqual(dn.status, "Cancelled") 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): def test_dn_billing_status_case1(self):
# SO -> DN -> SI # SO -> DN -> SI
so = make_sales_order(po_no="12345") 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) price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
# insert in database # 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: if args.price_list and args.rate:
insert_item_price(args) insert_item_price(args)
return out return out