Merge pull request #47429 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-05-06 19:39:35 +05:30
committed by GitHub
53 changed files with 844 additions and 318 deletions

View File

@@ -77,9 +77,12 @@
"reports_tab", "reports_tab",
"remarks_section", "remarks_section",
"general_ledger_remarks_length", "general_ledger_remarks_length",
"ignore_is_opening_check_for_reporting",
"column_break_lvjk", "column_break_lvjk",
"receivable_payable_remarks_length", "receivable_payable_remarks_length",
"accounts_receivable_payable_tuning_section",
"receivable_payable_fetch_method",
"legacy_section",
"ignore_is_opening_check_for_reporting",
"payment_request_settings", "payment_request_settings",
"create_pr_in_draft_status" "create_pr_in_draft_status"
], ],
@@ -532,6 +535,34 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Posting Date Inheritance for Exchange Gain / Loss", "label": "Posting Date Inheritance for Exchange Gain / Loss",
"options": "Invoice\nPayment\nReconciliation Date" "options": "Invoice\nPayment\nReconciliation Date"
},
{
"fieldname": "column_break_xrnd",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled, Sales Invoice will be generated instead of POS Invoice in POS Transactions for real-time update of G/L and Stock Ledger.",
"fieldname": "use_sales_invoice_in_pos",
"fieldtype": "Check",
"label": "Use Sales Invoice"
},
{
"default": "Buffered Cursor",
"fieldname": "receivable_payable_fetch_method",
"fieldtype": "Select",
"label": "Data Fetch Method",
"options": "Buffered Cursor\nUnBuffered Cursor"
},
{
"fieldname": "accounts_receivable_payable_tuning_section",
"fieldtype": "Section Break",
"label": "Accounts Receivable / Payable Tuning"
},
{
"fieldname": "legacy_section",
"fieldtype": "Section Break",
"label": "Legacy Fields"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@@ -539,7 +570,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-01-23 13:15:44.077853", "modified": "2025-05-05 12:29:38.302027",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -54,6 +54,7 @@ class AccountsSettings(Document):
merge_similar_account_heads: DF.Check merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency over_billing_allowance: DF.Currency
post_change_gl_entries: DF.Check post_change_gl_entries: DF.Check
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
receivable_payable_remarks_length: DF.Int receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int reconciliation_queue_size: DF.Int
role_allowed_to_over_bill: DF.Link | None role_allowed_to_over_bill: DF.Link | None

View File

@@ -7,6 +7,9 @@ import frappe
from frappe.utils import add_months, getdate from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
)
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
@@ -191,11 +194,13 @@ def make_pos_sales_invoice():
customer = make_customer(customer="_Test Customer") customer = make_customer(customer="_Test Customer")
mode_of_payment = frappe.get_doc("Mode of Payment", "Wire Transfer")
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank Clearance - _TC")
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1) si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
si.set("payments", []) si.set("payments", [])
si.append( si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 1000})
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank Clearance - _TC", "amount": 1000}
)
si.insert() si.insert()
si.submit() si.submit()

View File

@@ -12,6 +12,9 @@ from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool
get_linked_payments, get_linked_payments,
reconcile_vouchers, reconcile_vouchers,
) )
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
)
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
@@ -434,15 +437,13 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"}) mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Wire Transfer"})
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}): set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", gl_account)
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
mode_of_payment.save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1) si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1 si.is_pos = 1
si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080}) si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 109080})
si.insert() si.insert()
si.submit() si.submit()

View File

@@ -197,7 +197,7 @@ frappe.ui.form.on("Invoice Discounting", {
from_date: frm.doc.posting_date, from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"), to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company, company: frm.doc.company,
group_by: "Group by Voucher (Consolidated)", categorize_by: "Categorize by Voucher (Consolidated)",
show_cancelled_entries: frm.doc.docstatus === 2, show_cancelled_entries: frm.doc.docstatus === 2,
}; };
frappe.set_route("query-report", "General Ledger"); frappe.set_route("query-report", "General Ledger");

View File

@@ -35,7 +35,7 @@ frappe.ui.form.on("Journal Entry", {
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"), to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company, company: frm.doc.company,
finance_book: frm.doc.finance_book, finance_book: frm.doc.finance_book,
group_by: "", categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2, show_cancelled_entries: frm.doc.docstatus === 2,
}; };
frappe.set_route("query-report", "General Ledger"); frappe.set_route("query-report", "General Ledger");

View File

@@ -3,8 +3,25 @@
import unittest import unittest
# test_records = frappe.get_test_records('Mode of Payment') import frappe
class TestModeofPayment(unittest.TestCase): class TestModeofPayment(unittest.TestCase):
pass pass
def set_default_account_for_mode_of_payment(mode_of_payment, company, account):
mode_of_payment.reload()
if frappe.db.exists(
"Mode of Payment Account", {"parent": mode_of_payment.mode_of_payment, "company": company}
):
frappe.db.set_value(
"Mode of Payment Account",
{"parent": mode_of_payment.mode_of_payment, "company": company},
"default_account",
account,
)
return
mode_of_payment.append("accounts", {"company": company, "default_account": account})
mode_of_payment.save()

View File

@@ -411,7 +411,7 @@ frappe.ui.form.on("Payment Entry", {
from_date: frm.doc.posting_date, from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"), to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company, company: frm.doc.company,
group_by: "", categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2, show_cancelled_entries: frm.doc.docstatus === 2,
}; };
frappe.set_route("query-report", "General Ledger"); frappe.set_route("query-report", "General Ledger");

View File

@@ -1994,7 +1994,7 @@ class PaymentEntry(AccountsController):
# Re allocate amount to those references which have PR set (Higher priority) # Re allocate amount to those references which have PR set (Higher priority)
for ref in self.references: for ref in self.references:
if not ref.payment_request: if not (ref.reference_doctype and ref.reference_name and ref.payment_request):
continue continue
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount # fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
@@ -2045,7 +2045,7 @@ class PaymentEntry(AccountsController):
) )
# Re allocate amount to those references which have no PR (Lower priority) # Re allocate amount to those references which have no PR (Lower priority)
for ref in self.references: for ref in self.references:
if ref.payment_request: if ref.payment_request or not (ref.reference_doctype and ref.reference_name):
continue continue
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term")) key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))

View File

@@ -47,7 +47,7 @@ frappe.ui.form.on("Period Closing Voucher", {
from_date: frm.doc.posting_date, from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"), to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company, company: frm.doc.company,
group_by: "", categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2, show_cancelled_entries: frm.doc.docstatus === 2,
}; };
frappe.set_route("query-report", "General Ledger"); frappe.set_route("query-report", "General Ledger");

View File

@@ -323,3 +323,15 @@ frappe.ui.form.on("POS Invoice", {
}); });
}, },
}); });
frappe.ui.form.on("Sales Invoice Payment", {
mode_of_payment: function (frm) {
frappe.call({
doc: frm.doc,
method: "set_account_for_mode_of_payment",
callback: function (r) {
refresh_field("payments");
},
});
},
});

View File

@@ -7,6 +7,9 @@ import unittest
import frappe import frappe
from frappe import _ from frappe import _
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
)
from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
@@ -31,6 +34,8 @@ class TestPOSInvoice(unittest.TestCase):
cls.test_user, cls.pos_profile = init_user_and_profile() cls.test_user, cls.pos_profile = init_user_and_profile()
create_opening_entry(cls.pos_profile, cls.test_user) create_opening_entry(cls.pos_profile, cls.test_user)
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank - _TC")
def tearDown(self): def tearDown(self):
if frappe.session.user != "Administrator": if frappe.session.user != "Administrator":
@@ -233,12 +238,8 @@ class TestPOSInvoice(unittest.TestCase):
pos = create_pos_invoice(qty=10, do_not_save=True) pos = create_pos_invoice(qty=10, do_not_save=True)
pos.set("payments", []) pos.set("payments", [])
pos.append( pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 500})
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500} pos.append("payments", {"mode_of_payment": "Cash", "amount": 500, "default": 1})
)
pos.append(
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500, "default": 1}
)
pos.insert() pos.insert()
pos.submit() pos.submit()
@@ -276,9 +277,7 @@ class TestPOSInvoice(unittest.TestCase):
do_not_save=1, do_not_save=1,
) )
pos.append( pos.append("payments", {"mode_of_payment": "Cash", "amount": 1000, "default": 1})
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
)
pos.insert() pos.insert()
pos.submit() pos.submit()
@@ -318,9 +317,7 @@ class TestPOSInvoice(unittest.TestCase):
do_not_save=1, do_not_save=1,
) )
pos.append( pos.append("payments", {"mode_of_payment": "Cash", "amount": 2000, "default": 1})
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2000, "default": 1}
)
pos.insert() pos.insert()
pos.submit() pos.submit()
@@ -331,9 +328,7 @@ class TestPOSInvoice(unittest.TestCase):
# partial return 1 # partial return 1
pos_return1.get("items")[0].qty = -1 pos_return1.get("items")[0].qty = -1
pos_return1.set("payments", []) pos_return1.set("payments", [])
pos_return1.append( pos_return1.append("payments", {"mode_of_payment": "Cash", "amount": -1000, "default": 1})
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
)
pos_return1.paid_amount = -1000 pos_return1.paid_amount = -1000
pos_return1.submit() pos_return1.submit()
pos_return1.reload() pos_return1.reload()
@@ -350,9 +345,7 @@ class TestPOSInvoice(unittest.TestCase):
# partial return 2 # partial return 2
pos_return2 = make_sales_return(pos.name) pos_return2 = make_sales_return(pos.name)
pos_return2.set("payments", []) pos_return2.set("payments", [])
pos_return2.append( pos_return2.append("payments", {"mode_of_payment": "Cash", "amount": -1000, "default": 1})
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
)
pos_return2.paid_amount = -1000 pos_return2.paid_amount = -1000
pos_return2.submit() pos_return2.submit()
@@ -372,10 +365,8 @@ class TestPOSInvoice(unittest.TestCase):
) )
pos.set("payments", []) pos.set("payments", [])
pos.append("payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 50}) pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
pos.append( pos.append("payments", {"mode_of_payment": "Cash", "amount": 60, "default": 1})
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60, "default": 1}
)
pos.insert() pos.insert()
pos.submit() pos.submit()
@@ -393,7 +384,7 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv = create_pos_invoice(rate=10000, do_not_save=1) pos_inv = create_pos_invoice(rate=10000, do_not_save=1)
pos_inv.append( pos_inv.append(
"payments", "payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 9000}, {"mode_of_payment": "Cash", "amount": 9000},
) )
pos_inv.insert() pos_inv.insert()
self.assertRaises(PartialPaymentValidationError, pos_inv.submit) self.assertRaises(PartialPaymentValidationError, pos_inv.submit)
@@ -424,9 +415,7 @@ class TestPOSInvoice(unittest.TestCase):
do_not_save=1, do_not_save=1,
) )
pos.append( pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
)
pos.insert() pos.insert()
pos.submit() pos.submit()
@@ -445,9 +434,7 @@ class TestPOSInvoice(unittest.TestCase):
do_not_save=1, do_not_save=1,
) )
pos2.append( pos2.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
)
pos2.insert() pos2.insert()
self.assertRaises(frappe.ValidationError, pos2.submit) self.assertRaises(frappe.ValidationError, pos2.submit)
@@ -496,9 +483,7 @@ class TestPOSInvoice(unittest.TestCase):
do_not_save=1, do_not_save=1,
) )
pos2.append( pos2.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
)
pos2.insert() pos2.insert()
self.assertRaises(frappe.ValidationError, pos2.submit) self.assertRaises(frappe.ValidationError, pos2.submit)
@@ -561,9 +546,7 @@ class TestPOSInvoice(unittest.TestCase):
) )
pos.get("items")[0].has_serial_no = 1 pos.get("items")[0].has_serial_no = 1
pos.set("payments", []) pos.set("payments", [])
pos.append( pos.append("payments", {"mode_of_payment": "Cash", "amount": 1000, "default": 1})
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
)
pos = pos.save().submit() pos = pos.save().submit()
# make a return # make a return
@@ -609,7 +592,7 @@ class TestPOSInvoice(unittest.TestCase):
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1) inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
inv.append( inv.append(
"payments", "payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000}, {"mode_of_payment": "Cash", "amount": 10000},
) )
inv.insert() inv.insert()
inv.submit() inv.submit()
@@ -641,7 +624,7 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1) pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
pos_inv.append( pos_inv.append(
"payments", "payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000}, {"mode_of_payment": "Cash", "amount": 10000},
) )
pos_inv.paid_amount = 10000 pos_inv.paid_amount = 10000
pos_inv.submit() pos_inv.submit()
@@ -656,7 +639,7 @@ class TestPOSInvoice(unittest.TestCase):
inv.loyalty_amount = inv.loyalty_points * before_lp_details.conversion_factor inv.loyalty_amount = inv.loyalty_points * before_lp_details.conversion_factor
inv.append( inv.append(
"payments", "payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000 - inv.loyalty_amount}, {"mode_of_payment": "Cash", "amount": 10000 - inv.loyalty_amount},
) )
inv.paid_amount = 10000 inv.paid_amount = 10000
inv.submit() inv.submit()
@@ -677,12 +660,12 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1) pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270}) pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 270})
pos_inv.save() pos_inv.save()
pos_inv.submit() pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) pos_inv2.append("payments", {"mode_of_payment": "Cash", "amount": 3200})
pos_inv2.save() pos_inv2.save()
pos_inv2.submit() pos_inv2.submit()
@@ -703,7 +686,7 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1) pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
pos_inv.append( pos_inv.append(
"taxes", "taxes",
{ {
@@ -720,7 +703,7 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1) pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1)
pos_inv2.additional_discount_percentage = 10 pos_inv2.additional_discount_percentage = 10
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 540}) pos_inv2.append("payments", {"mode_of_payment": "Cash", "amount": 540})
pos_inv2.append( pos_inv2.append(
"taxes", "taxes",
{ {
@@ -758,7 +741,7 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1) pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
pos_inv.append( pos_inv.append(
"taxes", "taxes",
{ {
@@ -773,7 +756,7 @@ class TestPOSInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, pos_inv.submit) self.assertRaises(frappe.ValidationError, pos_inv.submit)
pos_inv2 = create_pos_invoice(item=item, rate=400, do_not_submit=1) pos_inv2 = create_pos_invoice(item=item, rate=400, do_not_submit=1)
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 400}) pos_inv2.append("payments", {"mode_of_payment": "Cash", "amount": 400})
pos_inv2.append( pos_inv2.append(
"taxes", "taxes",
{ {
@@ -818,7 +801,7 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1) pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1)
pos_inv1.append( pos_inv1.append(
"payments", "payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 4500}, {"mode_of_payment": "Cash", "amount": 4500},
) )
pos_inv1.items[0].batch_no = batch_no pos_inv1.items[0].batch_no = batch_no
pos_inv1.save() pos_inv1.save()
@@ -839,7 +822,7 @@ class TestPOSInvoice(unittest.TestCase):
) )
pos_inv2.append( pos_inv2.append(
"payments", "payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3000}, {"mode_of_payment": "Cash", "amount": 3000},
) )
pos_inv2.save() pos_inv2.save()
pos_inv2.submit() pos_inv2.submit()
@@ -879,7 +862,7 @@ class TestPOSInvoice(unittest.TestCase):
) )
pos_inv1.append( pos_inv1.append(
"payments", "payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}, {"mode_of_payment": "Cash", "amount": 300},
) )
pos_inv1.save() pos_inv1.save()
pos_inv1.submit() pos_inv1.submit()

View File

@@ -12,7 +12,7 @@
"posting_date", "posting_date",
"company", "company",
"account", "account",
"group_by", "categorize_by",
"cost_center", "cost_center",
"territory", "territory",
"ignore_exchange_rate_revaluation_journals", "ignore_exchange_rate_revaluation_journals",
@@ -174,14 +174,6 @@
"fieldtype": "Date", "fieldtype": "Date",
"label": "Start Date" "label": "Start Date"
}, },
{
"default": "Group by Voucher (Consolidated)",
"depends_on": "eval:(doc.report == 'General Ledger');",
"fieldname": "group_by",
"fieldtype": "Select",
"label": "Group By",
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
},
{ {
"depends_on": "eval: (doc.report == 'General Ledger');", "depends_on": "eval: (doc.report == 'General Ledger');",
"fieldname": "currency", "fieldname": "currency",
@@ -397,10 +389,18 @@
"fieldname": "show_remarks", "fieldname": "show_remarks",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Remarks" "label": "Show Remarks"
},
{
"default": "Categorize by Voucher (Consolidated)",
"depends_on": "eval:(doc.report == 'General Ledger');",
"fieldname": "categorize_by",
"fieldtype": "Select",
"label": "Categorize By",
"options": "\nCategorize by Voucher\nCategorize by Voucher (Consolidated)"
} }
], ],
"links": [], "links": [],
"modified": "2024-12-11 12:11:13.543134", "modified": "2025-04-30 14:43:23.643006",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Process Statement Of Accounts", "name": "Process Statement Of Accounts",

View File

@@ -44,6 +44,7 @@ class ProcessStatementOfAccounts(Document):
ageing_based_on: DF.Literal["Due Date", "Posting Date"] ageing_based_on: DF.Literal["Due Date", "Posting Date"]
based_on_payment_terms: DF.Check based_on_payment_terms: DF.Check
body: DF.TextEditor | None body: DF.TextEditor | None
categorize_by: DF.Literal["", "Categorize by Voucher", "Categorize by Voucher (Consolidated)"]
cc_to: DF.TableMultiSelect[ProcessStatementOfAccountsCC] cc_to: DF.TableMultiSelect[ProcessStatementOfAccountsCC]
collection_name: DF.DynamicLink | None collection_name: DF.DynamicLink | None
company: DF.Link company: DF.Link
@@ -56,7 +57,6 @@ class ProcessStatementOfAccounts(Document):
finance_book: DF.Link | None finance_book: DF.Link | None
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"] frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
from_date: DF.Date | None from_date: DF.Date | None
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
ignore_cr_dr_notes: DF.Check ignore_cr_dr_notes: DF.Check
ignore_exchange_rate_revaluation_journals: DF.Check ignore_exchange_rate_revaluation_journals: DF.Check
include_ageing: DF.Check include_ageing: DF.Check
@@ -204,7 +204,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
"party": [entry.customer], "party": [entry.customer],
"party_name": [entry.customer_name] if entry.customer_name else None, "party_name": [entry.customer_name] if entry.customer_name else None,
"presentation_currency": presentation_currency, "presentation_currency": presentation_currency,
"group_by": doc.group_by, "categorize_by": doc.categorize_by,
"currency": doc.currency, "currency": doc.currency,
"project": [p.project_name for p in doc.project], "project": [p.project_name for p in doc.project],
"show_opening_entries": 0, "show_opening_entries": 0,

View File

@@ -1084,6 +1084,18 @@ frappe.ui.form.on("Sales Invoice Timesheet", {
}, },
}); });
frappe.ui.form.on("Sales Invoice Payment", {
mode_of_payment: function (frm) {
frappe.call({
doc: frm.doc,
method: "set_account_for_mode_of_payment",
callback: function (r) {
refresh_field("payments");
},
});
},
});
var set_timesheet_detail_rate = function (cdt, cdn, currency, timelog) { var set_timesheet_detail_rate = function (cdt, cdn, currency, timelog) {
frappe.call({ frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_detail_rate", method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_detail_rate",

View File

@@ -751,9 +751,9 @@ class SalesInvoice(SellingController):
self.paid_amount = paid_amount self.paid_amount = paid_amount
self.base_paid_amount = base_paid_amount self.base_paid_amount = base_paid_amount
@frappe.whitelist()
def set_account_for_mode_of_payment(self): def set_account_for_mode_of_payment(self):
for payment in self.payments: for payment in self.payments:
if not payment.account:
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account") payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
def validate_time_sheets_are_submitted(self): def validate_time_sheets_are_submitted(self):

View File

@@ -12,6 +12,9 @@ from frappe.utils import add_days, flt, format_date, getdate, nowdate, today
import erpnext import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
)
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import WarehouseMissingError from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import WarehouseMissingError
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
@@ -54,6 +57,11 @@ class TestSalesInvoice(FrappeTestCase):
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
create_internal_parties() create_internal_parties()
setup_accounts() setup_accounts()
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank - _TC")
set_default_account_for_mode_of_payment(
mode_of_payment, "_Test Company with perpetual inventory", "_Test Bank - TCP1"
)
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
def tearDown(self): def tearDown(self):
@@ -964,10 +972,8 @@ class TestSalesInvoice(FrappeTestCase):
pos.is_pos = 1 pos.is_pos = 1
pos.update_stock = 1 pos.update_stock = 1
pos.append( pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50} pos.append("payments", {"mode_of_payment": "Cash", "amount": 50})
)
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 50})
taxes = get_taxes_and_charges() taxes = get_taxes_and_charges()
pos.taxes = [] pos.taxes = []
@@ -996,10 +1002,8 @@ class TestSalesInvoice(FrappeTestCase):
pos.is_pos = 1 pos.is_pos = 1
pos.pos_profile = pos_profile.name pos.pos_profile = pos_profile.name
pos.append( pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 500})
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500} pos.append("payments", {"mode_of_payment": "Cash", "amount": 500})
)
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
pos.insert() pos.insert()
pos.submit() pos.submit()
@@ -1042,10 +1046,8 @@ class TestSalesInvoice(FrappeTestCase):
pos.is_pos = 1 pos.is_pos = 1
pos.update_stock = 1 pos.update_stock = 1
pos.append( pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50} pos.append("payments", {"mode_of_payment": "Cash", "amount": 60})
)
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 60})
pos.write_off_outstanding_amount_automatically = 1 pos.write_off_outstanding_amount_automatically = 1
pos.insert() pos.insert()
@@ -1085,10 +1087,8 @@ class TestSalesInvoice(FrappeTestCase):
pos.is_pos = 1 pos.is_pos = 1
pos.update_stock = 1 pos.update_stock = 1
pos.append( pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50} pos.append("payments", {"mode_of_payment": "Cash", "amount": 40})
)
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 40})
pos.write_off_outstanding_amount_automatically = 1 pos.write_off_outstanding_amount_automatically = 1
pos.insert() pos.insert()
@@ -1102,7 +1102,7 @@ class TestSalesInvoice(FrappeTestCase):
pos = create_sales_invoice(do_not_save=True) pos = create_sales_invoice(do_not_save=True)
pos.is_pos = 1 pos.is_pos = 1
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) pos.append("payments", {"mode_of_payment": "Cash", "amount": 100})
pos.save().submit() pos.save().submit()
self.assertEqual(pos.outstanding_amount, 0.0) self.assertEqual(pos.outstanding_amount, 0.0)
self.assertEqual(pos.status, "Paid") self.assertEqual(pos.status, "Paid")
@@ -1173,10 +1173,8 @@ class TestSalesInvoice(FrappeTestCase):
for tax in taxes: for tax in taxes:
pos.append("taxes", tax) pos.append("taxes", tax)
pos.append( pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50} pos.append("payments", {"mode_of_payment": "Cash", "amount": 60})
)
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 60})
pos.insert() pos.insert()
pos.submit() pos.submit()
@@ -3904,10 +3902,8 @@ class TestSalesInvoice(FrappeTestCase):
pos = create_sales_invoice(qty=10, do_not_save=True) pos = create_sales_invoice(qty=10, do_not_save=True)
pos.is_pos = 1 pos.is_pos = 1
pos.pos_profile = pos_profile.name pos.pos_profile = pos_profile.name
pos.append( pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 500})
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500} pos.append("payments", {"mode_of_payment": "Cash", "amount": 500})
)
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
pos.save().submit() pos.save().submit()
pos_return = make_sales_return(pos.name) pos_return = make_sales_return(pos.name)
@@ -4278,7 +4274,7 @@ class TestSalesInvoice(FrappeTestCase):
pos.is_pos = 1 pos.is_pos = 1
pos.pos_profile = pos_profile.name pos.pos_profile = pos_profile.name
pos.debit_to = "_Test Receivable USD - _TC" pos.debit_to = "_Test Receivable USD - _TC"
pos.append("payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 20.35}) pos.append("payments", {"mode_of_payment": "Cash", "amount": 20.35})
pos.save().submit() pos.save().submit()
pos_return = make_sales_return(pos.name) pos_return = make_sales_return(pos.name)

View File

@@ -657,34 +657,34 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
return due_date return due_date
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None): def validate_due_date(posting_date, due_date, bill_date=None, template_name=None, doctype=None):
if getdate(due_date) < getdate(posting_date): if getdate(due_date) < getdate(posting_date):
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date")) frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
else: else:
validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype)
def validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype=None):
if not template_name: if not template_name:
return return
default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime( default_due_date = format(get_due_date_from_template(template_name, posting_date, bill_date))
"%Y-%m-%d"
)
if not default_due_date: if not default_due_date:
return return
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date): if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
is_credit_controller = ( if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles() party_type = "supplier" if doctype == "Purchase Invoice" else "customer"
)
if is_credit_controller:
msgprint( msgprint(
_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format( _("Note: Due Date exceeds allowed {0} credit days by {1} day(s)").format(
date_diff(due_date, default_due_date) party_type, date_diff(due_date, default_due_date)
) )
) )
else: else:
frappe.throw( frappe.throw(_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date)))
_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date))
)
@frappe.whitelist() @frappe.whitelist()
@@ -931,12 +931,16 @@ def get_party_shipping_address(doctype: str, name: str) -> str | None:
["is_shipping_address", "=", 1], ["is_shipping_address", "=", 1],
["address_type", "=", "Shipping"], ["address_type", "=", "Shipping"],
], ],
pluck="name", fields=["name", "is_shipping_address"],
limit=1,
order_by="is_shipping_address DESC", order_by="is_shipping_address DESC",
) )
return shipping_addresses[0] if shipping_addresses else None if shipping_addresses and shipping_addresses[0].is_shipping_address == 1:
return shipping_addresses[0].name
if len(shipping_addresses) == 1:
return shipping_addresses[0].name
else:
return None
def get_partywise_advanced_payment_amount( def get_partywise_advanced_payment_amount(

View File

@@ -54,6 +54,10 @@ class ReceivablePayableReport:
self.filters.range = "30, 60, 90, 120" self.filters.range = "30, 60, 90, 120"
self.ranges = [num.strip() for num in self.filters.range.split(",") if num.strip().isdigit()] self.ranges = [num.strip() for num in self.filters.range.split(",") if num.strip().isdigit()]
self.range_numbers = [num for num in range(1, len(self.ranges) + 2)] self.range_numbers = [num for num in range(1, len(self.ranges) + 2)]
self.ple_fetch_method = (
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
or "Buffered Cursor"
) # Fail Safe
def run(self, args): def run(self, args):
self.filters.update(args) self.filters.update(args)
@@ -90,10 +94,7 @@ class ReceivablePayableReport:
self.skip_total_row = 1 self.skip_total_row = 1
def get_data(self): def get_data(self):
self.get_ple_entries()
self.get_sales_invoices_or_customers_based_on_sales_person() self.get_sales_invoices_or_customers_based_on_sales_person()
self.voucher_balance = OrderedDict()
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
# Build delivery note map against all sales invoices # Build delivery note map against all sales invoices
self.build_delivery_note_map() self.build_delivery_note_map()
@@ -110,12 +111,40 @@ class ReceivablePayableReport:
# Get Exchange Rate Revaluations # Get Exchange Rate Revaluations
self.get_exchange_rate_revaluations() self.get_exchange_rate_revaluations()
self.prepare_ple_query()
self.data = [] self.data = []
self.voucher_balance = OrderedDict()
if self.ple_fetch_method == "Buffered Cursor":
self.fetch_ple_in_buffered_cursor()
elif self.ple_fetch_method == "UnBuffered Cursor":
self.fetch_ple_in_unbuffered_cursor()
self.build_data()
def fetch_ple_in_buffered_cursor(self):
self.ple_entries = frappe.db.sql(self.ple_query.get_sql(), as_dict=True)
for ple in self.ple_entries:
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
# This is unavoidable. Initialization and allocation cannot happen in same loop
for ple in self.ple_entries: for ple in self.ple_entries:
self.update_voucher_balance(ple) self.update_voucher_balance(ple)
self.build_data() delattr(self, "ple_entries")
def fetch_ple_in_unbuffered_cursor(self):
self.ple_entries = []
with frappe.db.unbuffered_cursor():
for ple in frappe.db.sql(self.ple_query.get_sql(), as_dict=True, as_iterator=True):
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
self.ple_entries.append(ple)
# This is unavoidable. Initialization and allocation cannot happen in same loop
for ple in self.ple_entries:
self.update_voucher_balance(ple)
delattr(self, "ple_entries")
def build_voucher_dict(self, ple): def build_voucher_dict(self, ple):
return frappe._dict( return frappe._dict(
@@ -136,11 +165,7 @@ class ReceivablePayableReport:
outstanding_in_account_currency=0.0, outstanding_in_account_currency=0.0,
) )
def init_voucher_balance(self): def init_voucher_balance(self, ple):
# build all keys, since we want to exclude vouchers beyond the report date
for ple in self.ple_entries:
# get the balance object for voucher_type
if self.filters.get("ignore_accounts"): if self.filters.get("ignore_accounts"):
key = (ple.voucher_type, ple.voucher_no, ple.party) key = (ple.voucher_type, ple.voucher_no, ple.party)
else: else:
@@ -778,7 +803,7 @@ class ReceivablePayableReport:
) )
row["range" + str(index + 1)] = row.outstanding row["range" + str(index + 1)] = row.outstanding
def get_ple_entries(self): def prepare_ple_query(self):
# get all the GL entries filtered by the given filters # get all the GL entries filtered by the given filters
self.prepare_conditions() self.prepare_conditions()
@@ -831,7 +856,7 @@ class ReceivablePayableReport:
else: else:
query = query.orderby(self.ple.posting_date, self.ple.party) query = query.orderby(self.ple.posting_date, self.ple.party)
self.ple_entries = query.run(as_dict=True) self.ple_query = query
def get_sales_invoices_or_customers_based_on_sales_person(self): def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"): if self.filters.get("sales_person"):

View File

@@ -144,10 +144,10 @@ class PartyLedgerSummaryReport:
if self.party_naming_by == "Naming Series": if self.party_naming_by == "Naming Series":
columns.append( columns.append(
{ {
"label": _(self.filters.party_type + "Name"), "label": _(self.filters.party_type + " Name"),
"fieldtype": "Data", "fieldtype": "Data",
"fieldname": "party_name", "fieldname": "party_name",
"width": 110, "width": 150,
} }
) )
@@ -252,12 +252,13 @@ class PartyLedgerSummaryReport:
self.party_data = frappe._dict({}) self.party_data = frappe._dict({})
for gle in self.gl_entries: for gle in self.gl_entries:
party_details = self.party_details.get(gle.party) party_details = self.party_details.get(gle.party)
party_name = party_details.get(f"{scrub(self.filters.party_type)}_name", "")
self.party_data.setdefault( self.party_data.setdefault(
gle.party, gle.party,
frappe._dict( frappe._dict(
{ {
**party_details, **party_details,
"party_name": gle.party, "party_name": party_name,
"opening_balance": 0, "opening_balance": 0,
"invoiced_amount": 0, "invoiced_amount": 0,
"paid_amount": 0, "paid_amount": 0,

View File

@@ -49,7 +49,7 @@ frappe.query_reports["General Ledger"] = {
label: __("Voucher No"), label: __("Voucher No"),
fieldtype: "Data", fieldtype: "Data",
on_change: function () { on_change: function () {
frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)"); frappe.query_report.set_filter_value("categorize_by", "Categorize by Voucher (Consolidated)");
}, },
}, },
{ {
@@ -112,29 +112,29 @@ frappe.query_reports["General Ledger"] = {
hidden: 1, hidden: 1,
}, },
{ {
fieldname: "group_by", fieldname: "categorize_by",
label: __("Group by"), label: __("Categorize by"),
fieldtype: "Select", fieldtype: "Select",
options: [ options: [
"", "",
{ {
label: __("Group by Voucher"), label: __("Categorize by Voucher"),
value: "Group by Voucher", value: "Categorize by Voucher",
}, },
{ {
label: __("Group by Voucher (Consolidated)"), label: __("Categorize by Voucher (Consolidated)"),
value: "Group by Voucher (Consolidated)", value: "Categorize by Voucher (Consolidated)",
}, },
{ {
label: __("Group by Account"), label: __("Categorize by Account"),
value: "Group by Account", value: "Categorize by Account",
}, },
{ {
label: __("Group by Party"), label: __("Categorize by Party"),
value: "Group by Party", value: "Categorize by Party",
}, },
], ],
default: "Group by Voucher (Consolidated)", default: "Categorize by Voucher (Consolidated)",
}, },
{ {
fieldname: "tax_id", fieldname: "tax_id",

View File

@@ -63,13 +63,17 @@ def validate_filters(filters, account_details):
if not account_details.get(account): if not account_details.get(account):
frappe.throw(_("Account {0} does not exists").format(account)) frappe.throw(_("Account {0} does not exists").format(account))
if filters.get("account") and filters.get("group_by") == "Group by Account": if not filters.get("categorize_by") and filters.get("group_by"):
filters["categorize_by"] = filters["group_by"]
filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by")
if filters.get("account") and filters.get("categorize_by") == "Categorize by Account":
filters.account = frappe.parse_json(filters.get("account")) filters.account = frappe.parse_json(filters.get("account"))
for account in filters.account: for account in filters.account:
if account_details[account].is_group == 0: if account_details[account].is_group == 0:
frappe.throw(_("Can not filter based on Child Account, if grouped by Account")) frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
if filters.get("voucher_no") and filters.get("group_by") in ["Group by Voucher"]: if filters.get("voucher_no") and filters.get("categorize_by") in ["Categorize by Voucher"]:
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher")) frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
if filters.from_date > filters.to_date: if filters.from_date > filters.to_date:
@@ -163,9 +167,9 @@ def get_gl_entries(filters, accounting_dimensions):
if filters.get("include_dimensions"): if filters.get("include_dimensions"):
order_by_statement = "order by posting_date, creation" order_by_statement = "order by posting_date, creation"
if filters.get("group_by") == "Group by Voucher": if filters.get("categorize_by") == "Categorize by Voucher":
order_by_statement = "order by posting_date, voucher_type, voucher_no" order_by_statement = "order by posting_date, voucher_type, voucher_no"
if filters.get("group_by") == "Group by Account": if filters.get("categorize_by") == "Categorize by Account":
order_by_statement = "order by account, posting_date, creation" order_by_statement = "order by account, posting_date, creation"
if filters.get("include_default_book_entries"): if filters.get("include_default_book_entries"):
@@ -260,7 +264,7 @@ def get_conditions(filters):
if filters.get("voucher_no_not_in"): if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s") conditions.append("voucher_no not in %(voucher_no_not_in)s")
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"): if filters.get("categorize_by") == "Categorize by Party" and not filters.get("party_type"):
conditions.append("party_type in ('Customer', 'Supplier')") conditions.append("party_type in ('Customer', 'Supplier')")
if filters.get("party_type"): if filters.get("party_type"):
@@ -272,7 +276,7 @@ def get_conditions(filters):
if not ( if not (
filters.get("account") filters.get("account")
or filters.get("party") or filters.get("party")
or filters.get("group_by") in ["Group by Account", "Group by Party"] or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
): ):
if not ignore_is_opening: if not ignore_is_opening:
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')") conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
@@ -374,26 +378,26 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
# Opening for filtered account # Opening for filtered account
data.append(totals.opening) data.append(totals.opening)
if filters.get("group_by") != "Group by Voucher (Consolidated)": if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
for _acc, acc_dict in gle_map.items(): for _acc, acc_dict in gle_map.items():
# acc # acc
if acc_dict.entries: if acc_dict.entries:
# opening # opening
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None}) data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
if (not filters.get("group_by") and not filters.get("voucher_no")) or ( if (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
filters.get("group_by") and filters.get("group_by") != "Group by Voucher" filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
): ):
data.append(acc_dict.totals.opening) data.append(acc_dict.totals.opening)
data += acc_dict.entries data += acc_dict.entries
# totals # totals
if filters.get("group_by") or not filters.voucher_no: if filters.get("categorize_by") or not filters.voucher_no:
data.append(acc_dict.totals.total) data.append(acc_dict.totals.total)
# closing # closing
if (not filters.get("group_by") and not filters.get("voucher_no")) or ( if (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
filters.get("group_by") and filters.get("group_by") != "Group by Voucher" filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
): ):
data.append(acc_dict.totals.closing) data.append(acc_dict.totals.closing)
@@ -430,9 +434,9 @@ def get_totals_dict():
def group_by_field(group_by): def group_by_field(group_by):
if group_by == "Group by Party": if group_by == "Categorize by Party":
return "party" return "party"
elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]: elif group_by in ["Categorize by Voucher (Consolidated)", "Categorize by Account"]:
return "account" return "account"
else: else:
return "voucher_no" return "voucher_no"
@@ -440,7 +444,7 @@ def group_by_field(group_by):
def initialize_gle_map(gl_entries, filters, totals_dict): def initialize_gle_map(gl_entries, filters, totals_dict):
gle_map = OrderedDict() gle_map = OrderedDict()
group_by = group_by_field(filters.get("group_by")) group_by = group_by_field(filters.get("categorize_by"))
for gle in gl_entries: for gle in gl_entries:
gle_map.setdefault(gle.get(group_by), _dict(totals=copy.deepcopy(totals_dict), entries=[])) gle_map.setdefault(gle.get(group_by), _dict(totals=copy.deepcopy(totals_dict), entries=[]))
@@ -450,8 +454,8 @@ def initialize_gle_map(gl_entries, filters, totals_dict):
def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals): def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals):
entries = [] entries = []
consolidated_gle = OrderedDict() consolidated_gle = OrderedDict()
group_by = group_by_field(filters.get("group_by")) group_by = group_by_field(filters.get("categorize_by"))
group_by_voucher_consolidated = filters.get("group_by") == "Group by Voucher (Consolidated)" group_by_voucher_consolidated = filters.get("categorize_by") == "Categorize by Voucher (Consolidated)"
if filters.get("show_net_values_in_party_account"): if filters.get("show_net_values_in_party_account"):
account_type_map = get_account_type_map(filters.get("company")) account_type_map = get_account_type_map(filters.get("company"))

View File

@@ -155,7 +155,7 @@ class TestGeneralLedger(FrappeTestCase):
"from_date": today(), "from_date": today(),
"to_date": today(), "to_date": today(),
"account": [account.name], "account": [account.name],
"group_by": "Group by Voucher (Consolidated)", "categorize_by": "Categorize by Voucher (Consolidated)",
} }
) )
) )
@@ -246,7 +246,7 @@ class TestGeneralLedger(FrappeTestCase):
"from_date": today(), "from_date": today(),
"to_date": today(), "to_date": today(),
"account": [account.name], "account": [account.name],
"group_by": "Group by Voucher (Consolidated)", "categorize_by": "Categorize by Voucher (Consolidated)",
"ignore_err": True, "ignore_err": True,
} }
) )
@@ -261,7 +261,7 @@ class TestGeneralLedger(FrappeTestCase):
"from_date": today(), "from_date": today(),
"to_date": today(), "to_date": today(),
"account": [account.name], "account": [account.name],
"group_by": "Group by Voucher (Consolidated)", "categorize_by": "Categorize by Voucher (Consolidated)",
"ignore_err": False, "ignore_err": False,
} }
) )
@@ -308,7 +308,7 @@ class TestGeneralLedger(FrappeTestCase):
"from_date": si.posting_date, "from_date": si.posting_date,
"to_date": si.posting_date, "to_date": si.posting_date,
"account": [si.debit_to], "account": [si.debit_to],
"group_by": "Group by Voucher (Consolidated)", "categorize_by": "Categorize by Voucher (Consolidated)",
"ignore_cr_dr_notes": False, "ignore_cr_dr_notes": False,
} }
) )
@@ -325,7 +325,7 @@ class TestGeneralLedger(FrappeTestCase):
"from_date": si.posting_date, "from_date": si.posting_date,
"to_date": si.posting_date, "to_date": si.posting_date,
"account": [si.debit_to], "account": [si.debit_to],
"group_by": "Group by Voucher (Consolidated)", "categorize_by": "Categorize by Voucher (Consolidated)",
"ignore_cr_dr_notes": True, "ignore_cr_dr_notes": True,
} }
) )

View File

@@ -12,8 +12,8 @@ DEFAULT_FILTERS = {
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}), ("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)"}),
("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}), ("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)", "include_dimensions": 1}),
("Accounts Payable", {"range": "30, 60, 90, 120"}), ("Accounts Payable", {"range": "30, 60, 90, 120"}),
("Accounts Receivable", {"range": "30, 60, 90, 120"}), ("Accounts Receivable", {"range": "30, 60, 90, 120"}),
("Consolidated Financial Statement", {"report": "Balance Sheet"}), ("Consolidated Financial Statement", {"report": "Balance Sheet"}),

View File

@@ -718,16 +718,15 @@ def get_gl_entries_on_asset_disposal(
def get_asset_details(asset, finance_book=None): def get_asset_details(asset, finance_book=None):
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts( value_after_depreciation = asset.get_value_after_depreciation(finance_book)
asset.asset_category, asset.company accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
fixed_asset_account, accumulated_depr_account, _ = get_asset_accounts(
asset.asset_category, asset.company, accumulated_depr_amount
) )
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
depreciation_cost_center = asset.cost_center or depreciation_cost_center depreciation_cost_center = asset.cost_center or depreciation_cost_center
value_after_depreciation = asset.get_value_after_depreciation(finance_book)
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
return ( return (
fixed_asset_account, fixed_asset_account,
asset, asset,
@@ -739,6 +738,48 @@ def get_asset_details(asset, finance_book=None):
) )
def get_asset_accounts(asset_category, company, accumulated_depr_amount):
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
accounts = frappe.db.get_value(
"Asset Category Account",
filters={"parent": asset_category, "company_name": company},
fieldname=[
"fixed_asset_account",
"accumulated_depreciation_account",
"depreciation_expense_account",
],
as_dict=1,
)
if accounts:
fixed_asset_account = accounts.fixed_asset_account
accumulated_depreciation_account = accounts.accumulated_depreciation_account
depreciation_expense_account = accounts.depreciation_expense_account
if not fixed_asset_account:
frappe.throw(_("Please set Fixed Asset Account in Asset Category {0}").format(asset_category))
if accumulated_depr_amount:
accounts = frappe.get_cached_value(
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
)
if not accumulated_depreciation_account:
accumulated_depreciation_account = accounts[0]
if not depreciation_expense_account:
depreciation_expense_account = accounts[1]
if not accumulated_depreciation_account or not depreciation_expense_account:
frappe.throw(
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
asset_category, company
)
)
return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
def get_profit_gl_entries( def get_profit_gl_entries(
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None
): ):

View File

@@ -76,14 +76,14 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
}, },
}, },
{ {
fieldname: "group_by", fieldname: "categorize_by",
label: __("Group by"), label: __("Categorize by"),
fieldtype: "Select", fieldtype: "Select",
options: [ options: [
{ label: __("Group by Supplier"), value: "Group by Supplier" }, { label: __("Categorize by Supplier"), value: "Categorize by Supplier" },
{ label: __("Group by Item"), value: "Group by Item" }, { label: __("Categorize by Item"), value: "Categorize by Item" },
], ],
default: __("Group by Supplier"), default: __("Categorize by Supplier"),
}, },
{ {
fieldtype: "Check", fieldtype: "Check",

View File

@@ -15,6 +15,8 @@ def execute(filters=None):
if not filters: if not filters:
return [], [] return [], []
validate_filters(filters)
columns = get_columns(filters) columns = get_columns(filters)
supplier_quotation_data = get_data(filters) supplier_quotation_data = get_data(filters)
@@ -24,6 +26,12 @@ def execute(filters=None):
return columns, data, message, chart_data return columns, data, message, chart_data
def validate_filters(filters):
if not filters.get("categorize_by") and filters.get("group_by"):
filters["categorize_by"] = filters["group_by"]
filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by")
def get_data(filters): def get_data(filters):
sq = frappe.qb.DocType("Supplier Quotation") sq = frappe.qb.DocType("Supplier Quotation")
sq_item = frappe.qb.DocType("Supplier Quotation Item") sq_item = frappe.qb.DocType("Supplier Quotation Item")
@@ -82,7 +90,9 @@ def prepare_data(supplier_quotation_data, filters):
group_wise_map = defaultdict(list) group_wise_map = defaultdict(list)
supplier_qty_price_map = {} supplier_qty_price_map = {}
group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code" group_by_field = (
"supplier_name" if filters.get("categorize_by") == "Categorize by Supplier" else "item_code"
)
float_precision = cint(frappe.db.get_default("float_precision")) or 2 float_precision = cint(frappe.db.get_default("float_precision")) or 2
for data in supplier_quotation_data: for data in supplier_quotation_data:
@@ -266,7 +276,7 @@ def get_columns(filters):
}, },
] ]
if filters.get("group_by") == "Group by Item": if filters.get("categorize_by") == "Categorize by Item":
group_by_columns.reverse() group_by_columns.reverse()
columns[0:0] = group_by_columns # add positioned group by columns to the report columns[0:0] = group_by_columns # add positioned group by columns to the report

View File

@@ -779,17 +779,10 @@ class AccountsController(TransactionBase):
if not self.due_date: if not self.due_date:
frappe.throw(_("Due Date is mandatory")) frappe.throw(_("Due Date is mandatory"))
validate_due_date( validate_due_date(posting_date, self.due_date, None, self.payment_terms_template, self.doctype)
posting_date,
self.due_date,
self.payment_terms_template,
)
elif self.doctype == "Purchase Invoice": elif self.doctype == "Purchase Invoice":
validate_due_date( validate_due_date(
posting_date, posting_date, self.due_date, self.bill_date, self.payment_terms_template, self.doctype
self.due_date,
self.bill_date,
self.payment_terms_template,
) )
def set_price_list_currency(self, buying_or_selling): def set_price_list_currency(self, buying_or_selling):

View File

@@ -402,3 +402,6 @@ erpnext.patches.v15_0.recalculate_amount_difference_field #2025-03-18
erpnext.patches.v15_0.rename_sla_fields #2025-03-12 erpnext.patches.v15_0.rename_sla_fields #2025-03-12
erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item
erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
erpnext.patches.v15_0.rename_group_by_to_categorize_by
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
erpnext.patches.v14_0.set_update_price_list_based_on

View File

@@ -0,0 +1,14 @@
import frappe
from frappe.utils import cint
def execute():
frappe.db.set_single_value(
"Stock Settings",
"update_price_list_based_on",
(
"Price List Rate"
if cint(frappe.db.get_single_value("Selling Settings", "editable_price_list_rate"))
else "Rate"
),
)

View File

@@ -0,0 +1,20 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
rename_field("Process Statement Of Accounts", "group_by", "categorize_by")
frappe.db.sql(
"""
UPDATE
`tabProcess Statement Of Accounts`
SET
categorize_by = CASE
WHEN categorize_by = 'Group by Voucher (Consolidated)' THEN 'Categorize by Voucher (Consolidated)'
WHEN categorize_by = 'Group by Voucher' THEN 'Categorize by Voucher'
END
WHERE
categorize_by IN ('Group by Voucher (Consolidated)', 'Group by Voucher')
"""
)

View File

@@ -513,7 +513,6 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20
user = frappe.session.user user = frappe.session.user
# find customer name from contact. # find customer name from contact.
customer = "" customer = ""
timesheets = []
contact = frappe.db.exists("Contact", {"user": user}) contact = frappe.db.exists("Contact", {"user": user})
if contact: if contact:
@@ -522,31 +521,43 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20
customer = contact.get_link_for("Customer") customer = contact.get_link_for("Customer")
if customer: if customer:
sales_invoices = [ sales_invoices = frappe.get_all("Sales Invoice", filters={"customer": customer}, pluck="name")
d.name for d in frappe.get_all("Sales Invoice", filters={"customer": customer}) projects = frappe.get_all("Project", filters={"customer": customer}, pluck="name")
] or [None]
projects = [d.name for d in frappe.get_all("Project", filters={"customer": customer})]
# Return timesheet related data to web portal.
timesheets = frappe.db.sql(
f"""
SELECT
ts.name, tsd.activity_type, ts.status, ts.total_billable_hours,
COALESCE(ts.sales_invoice, tsd.sales_invoice) AS sales_invoice, tsd.project
FROM `tabTimesheet` ts, `tabTimesheet Detail` tsd
WHERE tsd.parent = ts.name AND
(
ts.sales_invoice IN %(sales_invoices)s OR
tsd.sales_invoice IN %(sales_invoices)s OR
tsd.project IN %(projects)s
)
ORDER BY `end_date` ASC
LIMIT {limit_page_length} offset {limit_start}
""",
dict(sales_invoices=sales_invoices, projects=projects),
as_dict=True,
) # nosec
return timesheets # Return timesheet related data to web portal.
table = frappe.qb.DocType("Timesheet")
child_table = frappe.qb.DocType("Timesheet Detail")
query = (
frappe.qb.from_(table)
.join(child_table)
.on(table.name == child_table.parent)
.select(
table.name,
child_table.activity_type,
table.status,
table.total_billable_hours,
(table.sales_invoice | child_table.sales_invoice).as_("sales_invoice"),
child_table.project,
)
.orderby(table.end_date)
.limit(limit_page_length)
.offset(limit_start)
)
conditions = []
if sales_invoices:
conditions.extend(
[table.sales_invoice.isin(sales_invoices), child_table.sales_invoice.isin(sales_invoices)]
)
if projects:
conditions.append(child_table.project.isin(projects))
if conditions:
query = query.where(frappe.qb.terms.Criterion.any(conditions))
return query.run(as_dict=True)
else:
return {}
def get_list_context(context=None): def get_list_context(context=None):

View File

@@ -87,7 +87,7 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
from_date: me.frm.doc.posting_date, from_date: me.frm.doc.posting_date,
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'), to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
company: me.frm.doc.company, company: me.frm.doc.company,
group_by: "Group by Voucher (Consolidated)", categorize_by: "Categorize by Voucher (Consolidated)",
show_cancelled_entries: me.frm.doc.docstatus === 2, show_cancelled_entries: me.frm.doc.docstatus === 2,
ignore_prepared_report: true ignore_prepared_report: true
}; };

View File

@@ -1203,9 +1203,9 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
if (apply_sla_for_resolution) { if (apply_sla_for_resolution) {
let time_to_resolve; let time_to_resolve;
if (!frm.doc.resolution_date) { if (!frm.doc.resolution_date) {
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status); time_to_resolve = get_time_left(frm.doc.sla_resolution_by, frm.doc.agreement_status);
} else { } else {
time_to_resolve = get_status(frm.doc.resolution_by, frm.doc.resolution_date); time_to_resolve = get_status(frm.doc.sla_resolution_by, frm.doc.sla_resolution_date);
} }
alert += ` alert += `

View File

@@ -850,7 +850,13 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
def test_auto_insert_price(self): def test_auto_insert_price(self):
make_item("_Test Item for Auto Price List", {"is_stock_item": 0}) make_item("_Test Item for Auto Price List", {"is_stock_item": 0})
make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0}) make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0})
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1) frappe.db.set_single_value(
"Stock Settings",
{
"auto_insert_price_list_rate_if_missing": 1,
"update_price_list_based_on": "Price List Rate",
},
)
item_price = frappe.db.get_value( item_price = frappe.db.get_value(
"Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"} "Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}
@@ -862,6 +868,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100 item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100
) )
# ensure price gets inserted based on rate if price list rate is not defined by user
self.assertEqual( self.assertEqual(
frappe.db.get_value( frappe.db.get_value(
"Item Price", "Item Price",
@@ -871,6 +878,8 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
100, 100,
) )
# ensure price gets insterted based on user-defined *Price List Rate*
# if update_price_list_based_on is set to Price List Rate
make_sales_order( make_sales_order(
item_code="_Test Item for Auto Price List with Discount Percentage", item_code="_Test Item for Auto Price List with Discount Percentage",
selling_price_list="_Test Price List", selling_price_list="_Test Price List",
@@ -878,18 +887,43 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
discount_percentage=20, discount_percentage=20,
) )
self.assertEqual( item_price = frappe.db.get_value(
frappe.db.get_value(
"Item Price", "Item Price",
{ {
"price_list": "_Test Price List", "price_list": "_Test Price List",
"item_code": "_Test Item for Auto Price List with Discount Percentage", "item_code": "_Test Item for Auto Price List with Discount Percentage",
}, },
"price_list_rate", ("name", "price_list_rate"),
), as_dict=True,
200,
) )
self.assertEqual(item_price.price_list_rate, 200)
frappe.delete_doc("Item Price", item_price.name)
frappe.db.set_single_value("Stock Settings", "update_price_list_based_on", "Rate")
# ensure price gets insterted based on user-defined *Rate*
# if update_price_list_based_on is set to Rate
make_sales_order(
item_code="_Test Item for Auto Price List with Discount Percentage",
selling_price_list="_Test Price List",
price_list_rate=200,
discount_percentage=20,
)
item_price = frappe.db.get_value(
"Item Price",
{
"price_list": "_Test Price List",
"item_code": "_Test Item for Auto Price List with Discount Percentage",
},
("name", "price_list_rate"),
as_dict=True,
)
self.assertEqual(item_price.price_list_rate, 160)
frappe.delete_doc("Item Price", item_price.name)
# do not update price list # do not update price list
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0) frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
@@ -914,6 +948,63 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1) frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
def test_update_existing_item_price(self):
item_code = "_Test Item for Price List Updation"
price_list = "_Test Price List"
make_item(item_code, {"is_stock_item": 0})
frappe.db.set_single_value(
"Stock Settings",
{
"auto_insert_price_list_rate_if_missing": 1,
"update_existing_price_list_rate": 1,
"update_price_list_based_on": "Rate",
},
)
# setup: price creation
make_sales_order(item_code=item_code, selling_price_list=price_list, rate=100)
# test price updation based on Rate
make_sales_order(item_code=item_code, selling_price_list=price_list, rate=90)
self.assertEqual(
frappe.db.get_value(
"Item Price",
{"price_list": price_list, "item_code": item_code},
"price_list_rate",
),
90,
)
frappe.db.set_single_value(
"Stock Settings",
{
"update_price_list_based_on": "Price List Rate",
},
)
# test price updation based on Price List Rate
make_sales_order(
item_code=item_code,
selling_price_list=price_list,
price_list_rate=200,
discount_percentage=20,
)
self.assertEqual(
frappe.db.get_value(
"Item Price",
{"price_list": price_list, "item_code": item_code},
"price_list_rate",
),
200,
)
# reset `update_existing_price_list_rate` to 0
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
def test_drop_shipping(self): def test_drop_shipping(self):
from erpnext.buying.doctype.purchase_order.purchase_order import update_status from erpnext.buying.doctype.purchase_order.purchase_order import update_status
from erpnext.selling.doctype.sales_order.sales_order import ( from erpnext.selling.doctype.sales_order.sales_order import (

View File

@@ -2,5 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Selling Settings", { frappe.ui.form.on("Selling Settings", {
refresh: function (frm) {}, after_save(frm) {
frappe.boot.user.defaults.editable_price_list_rate = frm.doc.editable_price_list_rate;
},
}); });

View File

@@ -34,6 +34,7 @@ def set_default_settings(args):
stock_settings.stock_uom = _("Nos") stock_settings.stock_uom = _("Nos")
stock_settings.auto_indent = 1 stock_settings.auto_indent = 1
stock_settings.auto_insert_price_list_rate_if_missing = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1
stock_settings.update_price_list_based_on = "Rate"
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
stock_settings.save() stock_settings.save()

View File

@@ -502,6 +502,7 @@ def update_stock_settings():
stock_settings.stock_uom = _("Nos") stock_settings.stock_uom = _("Nos")
stock_settings.auto_indent = 1 stock_settings.auto_indent = 1
stock_settings.auto_insert_price_list_rate_if_missing = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1
stock_settings.update_price_list_based_on = "Rate"
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
stock_settings.save() stock_settings.save()

View File

@@ -223,6 +223,7 @@ def get_batch_qty(
ignore_voucher_nos=None, ignore_voucher_nos=None,
for_stock_levels=False, for_stock_levels=False,
consider_negative_batches=False, consider_negative_batches=False,
do_not_check_future_batches=False,
): ):
"""Returns batch actual qty if warehouse is passed, """Returns batch actual qty if warehouse is passed,
or returns dict of qty by warehouse if warehouse is None or returns dict of qty by warehouse if warehouse is None
@@ -249,6 +250,7 @@ def get_batch_qty(
"ignore_voucher_nos": ignore_voucher_nos, "ignore_voucher_nos": ignore_voucher_nos,
"for_stock_levels": for_stock_levels, "for_stock_levels": for_stock_levels,
"consider_negative_batches": consider_negative_batches, "consider_negative_batches": consider_negative_batches,
"do_not_check_future_batches": do_not_check_future_batches,
} }
) )

View File

@@ -7,6 +7,35 @@ const SALES_DOCTYPES = ["Quotation", "Sales Order", "Delivery Note", "Sales Invo
const PURCHASE_DOCTYPES = ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]; const PURCHASE_DOCTYPES = ["Purchase Order", "Purchase Receipt", "Purchase Invoice"];
frappe.ui.form.on("Item", { frappe.ui.form.on("Item", {
valuation_method(frm) {
if (!frm.is_new() && frm.doc.valuation_method === "Moving Average") {
let stock_exists = frm.doc.__onload && frm.doc.__onload.stock_exists ? 1 : 0;
let current_valuation_method = frm.doc.__onload.current_valuation_method;
if (stock_exists && current_valuation_method !== frm.doc.valuation_method) {
let msg = __(
"Changing the valuation method to Moving Average will affect new transactions. If backdated entries are added, earlier FIFO-based entries will be reposted, which may change closing balances."
);
msg += "<br>";
msg += __(
"Also you can't switch back to FIFO after setting the valuation method to Moving Average for this item."
);
msg += "<br>";
msg += __("Do you want to change valuation method?");
frappe.confirm(
msg,
() => {
frm.set_value("valuation_method", "Moving Average");
},
() => {
frm.set_value("valuation_method", current_valuation_method);
}
);
}
}
},
setup: function (frm) { setup: function (frm) {
frm.add_fetch("attribute", "numeric_values", "numeric_values"); frm.add_fetch("attribute", "numeric_values", "numeric_values");
frm.add_fetch("attribute", "from_range", "from_range"); frm.add_fetch("attribute", "from_range", "from_range");

View File

@@ -33,6 +33,7 @@ from erpnext.controllers.item_variant import (
validate_item_variant_attributes, validate_item_variant_attributes,
) )
from erpnext.stock.doctype.item_default.item_default import ItemDefault from erpnext.stock.doctype.item_default.item_default import ItemDefault
from erpnext.stock.utils import get_valuation_method
class DuplicateReorderRows(frappe.ValidationError): class DuplicateReorderRows(frappe.ValidationError):
@@ -153,6 +154,7 @@ class Item(Document):
def onload(self): def onload(self):
self.set_onload("stock_exists", self.stock_ledger_created()) self.set_onload("stock_exists", self.stock_ledger_created())
self.set_onload("asset_naming_series", get_asset_naming_series()) self.set_onload("asset_naming_series", get_asset_naming_series())
self.set_onload("current_valuation_method", get_valuation_method(self.name))
def autoname(self): def autoname(self):
if frappe.db.get_default("item_naming_by") == "Naming Series": if frappe.db.get_default("item_naming_by") == "Naming Series":

View File

@@ -462,6 +462,7 @@ frappe.ui.form.on("Stock Entry", {
docstatus: 1, docstatus: 1,
purpose: "Material Transfer", purpose: "Material Transfer",
add_to_transit: 1, add_to_transit: 1,
per_transferred: ["<", 100],
}, },
}); });
}, },

View File

@@ -503,17 +503,29 @@ class StockEntry(StockController):
).format(frappe.bold(self.company)) ).format(frappe.bold(self.company))
) )
elif ( acc_details = frappe.get_cached_value(
self.is_opening == "Yes" "Account",
and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss" d.expense_account,
): ["account_type", "report_type"],
as_dict=True,
)
if self.is_opening == "Yes" and acc_details.report_type == "Profit and Loss":
frappe.throw( frappe.throw(
_( _(
"Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry" "Difference Account must be a Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry"
), ),
OpeningEntryAccountError, OpeningEntryAccountError,
) )
if acc_details.account_type == "Stock":
frappe.throw(
_(
"At row {0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account"
).format(d.idx, get_link_to_form("Account", d.expense_account)),
OpeningEntryAccountError,
)
def validate_warehouse(self): def validate_warehouse(self):
"""perform various (sometimes conditional) validations on warehouse""" """perform various (sometimes conditional) validations on warehouse"""

View File

@@ -1314,7 +1314,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
# To deliver 100 qty we fall short of 11.0073 qty (11.007 with precision 3) # To deliver 100 qty we fall short of 11.0073 qty (11.007 with precision 3)
# Stock up with 11.007 (balance in db becomes 99.9997, on UI it will show as 100) # Stock up with 11.007 (balance in db becomes 99.9997, on UI it will show as 100)
make_stock_entry(item_code=item_code, target=warehouse, qty=11.007, rate=100) make_stock_entry(item_code=item_code, target=warehouse, qty=11.007, rate=100)
self.assertEqual(get_stock_balance(item_code, warehouse), 99.9997) self.assertEqual(get_stock_balance(item_code, warehouse), 100.0)
# See if delivery note goes through # See if delivery note goes through
# Negative qty error should not be raised as 99.9997 is 100 with precision 3 (system precision) # Negative qty error should not be raised as 99.9997 is 100 with precision 3 (system precision)

View File

@@ -5,11 +5,11 @@
import frappe import frappe
from frappe import _, bold, json, msgprint from frappe import _, bold, json, msgprint
from frappe.query_builder.functions import CombineDatetime, Sum from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import add_to_date, cint, cstr, flt from frappe.utils import add_to_date, cint, cstr, flt, get_datetime
import erpnext import erpnext
from erpnext.accounts.utils import get_company_default from erpnext.accounts.utils import get_company_default
from erpnext.controllers.stock_controller import StockController from erpnext.controllers.stock_controller import StockController, create_repost_item_valuation_entry
from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
@@ -186,9 +186,35 @@ class StockReconciliation(StockController):
if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle: if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle:
bundle = self.get_bundle_for_specific_serial_batch(item) bundle = self.get_bundle_for_specific_serial_batch(item)
if not bundle:
continue
item.current_serial_and_batch_bundle = bundle.name item.current_serial_and_batch_bundle = bundle.name
item.current_valuation_rate = abs(bundle.avg_rate) item.current_valuation_rate = abs(bundle.avg_rate)
if bundle.total_qty:
item.current_qty = abs(bundle.total_qty)
if save:
if not item.current_qty:
frappe.throw(
_("Row # {0}: Please enter quantity for Item {1} as it is not zero.").format(
item.idx, item.item_code
)
)
if self.docstatus == 1:
bundle.voucher_no = self.name
bundle.submit()
item.db_set(
{
"current_serial_and_batch_bundle": item.current_serial_and_batch_bundle,
"current_qty": item.current_qty,
"current_valuation_rate": item.current_valuation_rate,
}
)
if not item.valuation_rate: if not item.valuation_rate:
item.valuation_rate = item.current_valuation_rate item.valuation_rate = item.current_valuation_rate
continue continue
@@ -333,15 +359,21 @@ class StockReconciliation(StockController):
entry.batch_no, entry.batch_no,
row.warehouse, row.warehouse,
row.item_code, row.item_code,
ignore_voucher_nos=[self.name],
posting_date=self.posting_date, posting_date=self.posting_date,
posting_time=self.posting_time, posting_time=self.posting_time,
for_stock_levels=True, for_stock_levels=True,
consider_negative_batches=True, consider_negative_batches=True,
do_not_check_future_batches=True,
) )
if not current_qty:
continue
total_current_qty += current_qty total_current_qty += current_qty
entry.qty = current_qty * -1 entry.qty = current_qty * -1
if total_current_qty:
reco_obj.save() reco_obj.save()
row.current_qty = total_current_qty row.current_qty = total_current_qty
@@ -968,7 +1000,7 @@ class StockReconciliation(StockController):
else: else:
self._cancel() self._cancel()
def recalculate_current_qty(self, voucher_detail_no): def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
for row in self.items: for row in self.items:
@@ -1036,6 +1068,49 @@ class StockReconciliation(StockController):
} }
) )
if (
add_new_sle
and not frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
"name",
)
and not row.current_serial_and_batch_bundle
):
self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
row.reload()
self.add_missing_stock_ledger_entry(row, voucher_detail_no, sle_creation)
def add_missing_stock_ledger_entry(self, row, voucher_detail_no, sle_creation):
if row.current_qty == 0:
return
new_sle = frappe.get_doc(self.get_sle_for_items(row))
new_sle.actual_qty = row.current_qty * -1
new_sle.valuation_rate = row.current_valuation_rate
new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle
new_sle.submit()
creation = add_to_date(sle_creation, seconds=-1)
new_sle.db_set("creation", creation)
if not frappe.db.exists(
"Repost Item Valuation",
{"item": row.item_code, "warehouse": row.warehouse, "docstatus": 1, "status": "Queued"},
):
create_repost_item_valuation_entry(
{
"based_on": "Item and Warehouse",
"item_code": row.item_code,
"warehouse": row.warehouse,
"company": self.company,
"allow_negative_stock": 1,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
}
)
def has_negative_stock_allowed(self): def has_negative_stock_allowed(self):
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
if allow_negative_stock: if allow_negative_stock:
@@ -1109,6 +1184,7 @@ class StockReconciliation(StockController):
ignore_voucher_nos=[doc.voucher_no], ignore_voucher_nos=[doc.voucher_no],
for_stock_levels=True, for_stock_levels=True,
consider_negative_batches=True, consider_negative_batches=True,
do_not_check_future_batches=True,
) )
or 0 or 0
) * -1 ) * -1

View File

@@ -1069,7 +1069,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
sr.reload() sr.reload()
self.assertTrue(sr.items[0].serial_and_batch_bundle) self.assertTrue(sr.items[0].serial_and_batch_bundle)
self.assertFalse(sr.items[0].current_serial_and_batch_bundle) self.assertTrue(sr.items[0].current_serial_and_batch_bundle)
def test_not_reconcile_all_batch(self): def test_not_reconcile_all_batch(self):
from erpnext.stock.doctype.batch.batch import get_batch_qty from erpnext.stock.doctype.batch.batch import get_batch_qty
@@ -1446,6 +1446,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(sr.difference_amount, 100 * -1) self.assertEqual(sr.difference_amount, 100 * -1)
self.assertTrue(sr.items[0].qty == 0) self.assertTrue(sr.items[0].qty == 0)
def test_stock_reco_recalculate_qty_for_backdated_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item_code = self.make_item(
"Test Batch Item Stock Reco Recalculate Qty",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TEST-BATCH-RRQ-.###",
},
).name
warehouse = "_Test Warehouse - _TC"
sr = create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=10,
rate=100,
use_serial_batch_fields=1,
)
sr.reload()
self.assertEqual(sr.items[0].current_qty, 0)
self.assertEqual(sr.items[0].current_valuation_rate, 0)
batch_no = get_batch_from_bundle(sr.items[0].serial_and_batch_bundle)
stock_ledgers = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": sr.name, "is_cancelled": 0},
pluck="name",
)
self.assertTrue(len(stock_ledgers) == 1)
make_stock_entry(
item_code=item_code,
target=warehouse,
qty=10,
basic_rate=100,
use_serial_batch_fields=1,
batch_no=batch_no,
)
# Make backdated stock reconciliation entry
create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=10,
rate=100,
use_serial_batch_fields=1,
batch_no=batch_no,
posting_date=add_days(nowdate(), -1),
)
stock_ledgers = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": sr.name, "is_cancelled": 0},
pluck="name",
)
sr.reload()
self.assertEqual(sr.items[0].current_qty, 10)
self.assertEqual(sr.items[0].current_valuation_rate, 100)
self.assertTrue(len(stock_ledgers) == 2)
def create_batch_item_with_batch(item_name, batch_id): def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1) batch_item_doc = create_item(item_name, is_stock_item=1)

View File

@@ -51,4 +51,30 @@ frappe.ui.form.on("Stock Settings", {
} }
); );
}, },
auto_insert_price_list_rate_if_missing(frm) {
if (!frm.doc.auto_insert_price_list_rate_if_missing) return;
frm.set_value(
"update_price_list_based_on",
cint(frappe.defaults.get_default("editable_price_list_rate")) ? "Price List Rate" : "Rate"
);
},
update_price_list_based_on(frm) {
if (
frm.doc.update_price_list_based_on === "Price List Rate" &&
!cint(frappe.defaults.get_default("editable_price_list_rate"))
) {
const dialog = frappe.warn(
__("Incompatible Setting Detected"),
__(
"<p>Price List Rate has not been set as editable in Selling Settings. In this scenario, setting <strong>Update Price List Based On</strong> to <strong>Price List Rate</strong> will prevent auto-updation of Item Price.</p>Are you sure you want to continue?"
)
);
dialog.set_secondary_action(() => {
frm.set_value("update_price_list_based_on", "Rate");
dialog.hide();
});
return;
}
},
}); });

View File

@@ -16,6 +16,7 @@
"stock_uom", "stock_uom",
"price_list_defaults_section", "price_list_defaults_section",
"auto_insert_price_list_rate_if_missing", "auto_insert_price_list_rate_if_missing",
"update_price_list_based_on",
"column_break_12", "column_break_12",
"update_existing_price_list_rate", "update_existing_price_list_rate",
"conversion_factor_section", "conversion_factor_section",
@@ -531,6 +532,15 @@
"fieldname": "allow_to_make_quality_inspection_after_purchase_or_delivery", "fieldname": "allow_to_make_quality_inspection_after_purchase_or_delivery",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow to Make Quality Inspection after Purchase / Delivery" "label": "Allow to Make Quality Inspection after Purchase / Delivery"
},
{
"default": "Rate",
"depends_on": "eval: doc.auto_insert_price_list_rate_if_missing",
"fieldname": "update_price_list_based_on",
"fieldtype": "Select",
"label": "Update Price List Based On",
"mandatory_depends_on": "eval: doc.auto_insert_price_list_rate_if_missing",
"options": "Rate\nPrice List Rate"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@@ -538,7 +548,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-04-11 18:56:35.781929", "modified": "2025-05-06 02:39:24.284587",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@@ -64,6 +64,7 @@ class StockSettings(Document):
stock_frozen_upto_days: DF.Int stock_frozen_upto_days: DF.Int
stock_uom: DF.Link | None stock_uom: DF.Link | None
update_existing_price_list_rate: DF.Check update_existing_price_list_rate: DF.Check
update_price_list_based_on: DF.Literal["Rate", "Price List Rate"]
use_naming_series: DF.Check use_naming_series: DF.Check
use_serial_batch_fields: DF.Check use_serial_batch_fields: DF.Check
valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"] valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"]

View File

@@ -911,8 +911,8 @@ 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 or frappe.db.get_single_value( if price_list_rate is None or frappe.get_cached_value(
"Stock Settings", "update_existing_price_list_rate" "Stock Settings", "Stock Settings", "update_existing_price_list_rate"
): ):
insert_item_price(args) insert_item_price(args)
@@ -946,15 +946,14 @@ def insert_item_price(args):
): ):
return return
if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency and cint( stock_settings = frappe.get_cached_doc("Stock Settings")
frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")
if (
not frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency
or not stock_settings.auto_insert_price_list_rate_if_missing
or not frappe.has_permission("Item Price", "write")
): ):
if frappe.has_permission("Item Price", "write"): return
price_list_rate = (
(flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor")
if args.get("conversion_factor")
else (flt(args.rate) + flt(args.discount_amount))
)
item_price = frappe.db.get_value( item_price = frappe.db.get_value(
"Item Price", "Item Price",
@@ -967,18 +966,32 @@ def insert_item_price(args):
["name", "price_list_rate"], ["name", "price_list_rate"],
as_dict=1, as_dict=1,
) )
update_based_on_price_list_rate = stock_settings.update_price_list_based_on == "Price List Rate"
if item_price and item_price.name: if item_price and item_price.name:
if item_price.price_list_rate != price_list_rate and frappe.db.get_single_value( if not stock_settings.update_existing_price_list_rate:
"Stock Settings", "update_existing_price_list_rate" return
):
rate_to_consider = flt(args.price_list_rate) if update_based_on_price_list_rate else flt(args.rate)
price_list_rate = _get_stock_uom_rate(rate_to_consider, args)
if not price_list_rate or item_price.price_list_rate == price_list_rate:
return
frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate) frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate)
frappe.msgprint( frappe.msgprint(
_("Item Price updated for {0} in Price List {1}").format( _("Item Price updated for {0} in Price List {1}").format(args.item_code, args.price_list),
args.item_code, args.price_list
),
alert=True, alert=True,
) )
else: else:
rate_to_consider = (
(flt(args.price_list_rate) or flt(args.rate))
if update_based_on_price_list_rate
else flt(args.rate)
)
price_list_rate = _get_stock_uom_rate(rate_to_consider, args)
item_price = frappe.get_doc( item_price = frappe.get_doc(
{ {
"doctype": "Item Price", "doctype": "Item Price",
@@ -996,6 +1009,10 @@ def insert_item_price(args):
) )
def _get_stock_uom_rate(rate, args):
return rate / args.conversion_factor if args.conversion_factor else rate
def get_item_price(args, item_code, ignore_party=False, force_batch_no=False) -> list[dict]: def get_item_price(args, item_code, ignore_party=False, force_batch_no=False) -> list[dict]:
""" """
Get name, price_list_rate from Item Price based on conditions Get name, price_list_rate from Item Price based on conditions

View File

@@ -743,6 +743,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
if not self.sle.actual_qty: if not self.sle.actual_qty:
self.sle.actual_qty = self.get_actual_qty() self.sle.actual_qty = self.get_actual_qty()
if not self.sle.actual_qty:
return 0.0
return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty)) return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
def get_actual_qty(self): def get_actual_qty(self):

View File

@@ -894,7 +894,7 @@ class update_entries_after:
self.wh_data.prev_stock_value = self.wh_data.stock_value self.wh_data.prev_stock_value = self.wh_data.stock_value
# update current sle # update current sle
sle.qty_after_transaction = self.wh_data.qty_after_transaction sle.qty_after_transaction = flt(self.wh_data.qty_after_transaction, self.flt_precision)
sle.valuation_rate = self.wh_data.valuation_rate sle.valuation_rate = self.wh_data.valuation_rate
sle.stock_value = self.wh_data.stock_value sle.stock_value = self.wh_data.stock_value
sle.stock_queue = json.dumps(self.wh_data.stock_queue) sle.stock_queue = json.dumps(self.wh_data.stock_queue)
@@ -962,7 +962,7 @@ class update_entries_after:
def reset_actual_qty_for_stock_reco(self, sle): def reset_actual_qty_for_stock_reco(self, sle):
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
doc.recalculate_current_qty(sle.voucher_detail_no) doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
if sle.actual_qty < 0: if sle.actual_qty < 0:
sle.actual_qty = ( sle.actual_qty = (

View File

@@ -54,7 +54,7 @@ frappe.ui.form.on("Subcontracting Receipt", {
from_date: frm.doc.posting_date, from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"), to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company, company: frm.doc.company,
group_by: "Group by Voucher (Consolidated)", categorize_by: "Categorize by Voucher (Consolidated)",
show_cancelled_entries: frm.doc.docstatus === 2, show_cancelled_entries: frm.doc.docstatus === 2,
}; };
frappe.set_route("query-report", "General Ledger"); frappe.set_route("query-report", "General Ledger");