mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-11 08:53:03 +00:00
Compare commits
41 Commits
version-16
...
v16.22.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
054b20a2ae | ||
|
|
03f6b7a50e | ||
|
|
6bcab7cfc8 | ||
|
|
39c8161011 | ||
|
|
656d1bd6e3 | ||
|
|
ecb572de92 | ||
|
|
23181b3962 | ||
|
|
4af265c48f | ||
|
|
ce97a74c5f | ||
|
|
e955b4a3b9 | ||
|
|
de42a9e86e | ||
|
|
5206b279b6 | ||
|
|
9af618d6bf | ||
|
|
3c3cde4362 | ||
|
|
d6b7791f18 | ||
|
|
ff46d20b25 | ||
|
|
d215fa7623 | ||
|
|
bf8f7ba883 | ||
|
|
6ef4a2d82c | ||
|
|
e4e796146d | ||
|
|
ea1d0cc277 | ||
|
|
fa4aa0c1b6 | ||
|
|
fdfcbf72bd | ||
|
|
fb7f820885 | ||
|
|
799d6d159c | ||
|
|
48f59a033f | ||
|
|
2807c9f08f | ||
|
|
5271773595 | ||
|
|
dd35cd1f84 | ||
|
|
77a6299e8b | ||
|
|
b79ec7cbdd | ||
|
|
927360dd1d | ||
|
|
ab090295d9 | ||
|
|
c4b7b15824 | ||
|
|
cfd3847255 | ||
|
|
dc914adb62 | ||
|
|
41bff45d7a | ||
|
|
7b494dc9e8 | ||
|
|
ed69dafbe8 | ||
|
|
4d5c665e22 | ||
|
|
e09487d140 |
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "16.15.1"
|
||||
__version__ = "16.22.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -2294,9 +2294,6 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
if args.get("party_type") == "Member":
|
||||
return
|
||||
|
||||
if args.get("party_type") and args.get("party"):
|
||||
frappe.has_permission(args["party_type"], "read", args["party"], throw=True)
|
||||
|
||||
if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"):
|
||||
args["get_outstanding_invoices"] = True
|
||||
|
||||
@@ -2788,7 +2785,6 @@ def get_reference_details(
|
||||
):
|
||||
total_amount = outstanding_amount = exchange_rate = account = None
|
||||
|
||||
frappe.has_permission(reference_doctype, "read", reference_name, throw=True)
|
||||
ref_doc = frappe.get_lazy_doc(reference_doctype, reference_name)
|
||||
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
|
||||
|
||||
|
||||
@@ -93,6 +93,8 @@ def get_party_details(
|
||||
):
|
||||
if not party:
|
||||
return frappe._dict()
|
||||
if not frappe.db.exists(party_type, party):
|
||||
frappe.throw(_("{0}: {1} does not exists").format(party_type, party))
|
||||
return _get_party_details(
|
||||
party,
|
||||
account,
|
||||
@@ -103,7 +105,7 @@ def get_party_details(
|
||||
price_list,
|
||||
currency,
|
||||
doctype,
|
||||
False,
|
||||
ignore_permissions,
|
||||
fetch_payment_terms_template,
|
||||
party_address,
|
||||
company_address,
|
||||
|
||||
@@ -928,28 +928,8 @@ class ReceivablePayableReport:
|
||||
if self.filters.project:
|
||||
self.qb_selection_filter.append(self.ple.project.isin(self.filters.project))
|
||||
|
||||
self.add_user_permission_filters()
|
||||
|
||||
self.add_accounting_dimensions_filters()
|
||||
|
||||
def add_user_permission_filters(self):
|
||||
# Party is a dynamic link, so match conditions cannot auto-apply Customer/Supplier user permissions
|
||||
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
|
||||
from frappe.permissions import get_allowed_docs_for_doctype
|
||||
|
||||
user_permissions = get_user_permissions()
|
||||
if not user_permissions:
|
||||
return
|
||||
|
||||
for party_type in self.party_type:
|
||||
if party_type not in user_permissions:
|
||||
continue
|
||||
|
||||
allowed_parties = get_allowed_docs_for_doctype(user_permissions[party_type], party_type)
|
||||
self.qb_selection_filter.append(
|
||||
(self.ple.party_type != party_type) | self.ple.party.isin(allowed_parties or [""])
|
||||
)
|
||||
|
||||
def get_cost_center_conditions(self):
|
||||
cost_center_list = get_cost_centers_with_children(self.filters.cost_center)
|
||||
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
|
||||
|
||||
@@ -1245,44 +1245,3 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])
|
||||
|
||||
def test_accounts_receivable_respects_user_permissions(self):
|
||||
# Party is a dynamic link on Payment Ledger Entry, so user permissions on Customer
|
||||
# must be applied explicitly. The report should only show permitted customers.
|
||||
original_customer = self.customer
|
||||
second_customer = "_Test AR Perm Customer"
|
||||
|
||||
# create_customer overrides self.customer, so build the restricted invoice first
|
||||
self.create_customer(customer_name=second_customer)
|
||||
self.create_sales_invoice(no_payment_schedule=True)
|
||||
|
||||
self.customer = original_customer
|
||||
allowed_invoice = self.create_sales_invoice(no_payment_schedule=True)
|
||||
|
||||
test_user = "test_ar_user_permission@example.com"
|
||||
if not frappe.db.exists("User", test_user):
|
||||
user = frappe.new_doc("User")
|
||||
user.email = test_user
|
||||
user.first_name = "AR Perm"
|
||||
user.append("roles", {"role": "Accounts User"})
|
||||
user.save()
|
||||
|
||||
frappe.permissions.add_user_permission("Customer", original_customer, test_user)
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
frappe.set_user(test_user)
|
||||
try:
|
||||
report = execute(filters)
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
parties = {row.party for row in report[1]}
|
||||
self.assertIn(original_customer, parties)
|
||||
self.assertNotIn(second_customer, parties)
|
||||
self.assertEqual(allowed_invoice.customer, original_customer)
|
||||
|
||||
@@ -303,7 +303,6 @@ def get_balance_on(
|
||||
)
|
||||
|
||||
if party_type and party:
|
||||
frappe.has_permission(party_type, "read", party, throw=True)
|
||||
cond.append(
|
||||
f"""gle.party_type = {frappe.db.escape(party_type)} and gle.party = {frappe.db.escape(party)} """
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ from frappe.utils import get_url
|
||||
from frappe.utils.print_format import download_pdf
|
||||
from frappe.utils.user import get_user_fullname
|
||||
|
||||
from erpnext.accounts.party import _get_party_details, get_party_account_currency
|
||||
from erpnext.accounts.party import get_party_account_currency, get_party_details
|
||||
from erpnext.buying.utils import validate_for_items
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.stock.doctype.material_request.material_request import set_missing_values
|
||||
@@ -447,7 +447,7 @@ def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=
|
||||
def postprocess(source, target_doc):
|
||||
if for_supplier:
|
||||
target_doc.supplier = for_supplier
|
||||
args = _get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True)
|
||||
args = get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True)
|
||||
target_doc.currency = args.currency or get_party_account_currency(
|
||||
"Supplier", for_supplier, source.company
|
||||
)
|
||||
|
||||
@@ -119,12 +119,12 @@ class TestSupplier(ERPNextTestSuite):
|
||||
self.assertEqual(supplier.country, "Greece")
|
||||
|
||||
def test_party_details_tax_category(self):
|
||||
from erpnext.accounts.party import _get_party_details
|
||||
from erpnext.accounts.party import get_party_details
|
||||
|
||||
frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing")
|
||||
|
||||
# Tax Category without Address
|
||||
details = _get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
|
||||
details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
|
||||
self.assertEqual(details.tax_category, "_Test Tax Category 1")
|
||||
|
||||
address = frappe.get_doc(
|
||||
@@ -139,7 +139,7 @@ class TestSupplier(ERPNextTestSuite):
|
||||
).insert()
|
||||
|
||||
# Tax Category with Address
|
||||
details = _get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
|
||||
details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
|
||||
self.assertEqual(details.tax_category, "_Test Tax Category 2")
|
||||
|
||||
# Rollback
|
||||
|
||||
@@ -13,7 +13,7 @@ from frappe.utils.data import nowtime
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.party import _get_party_details
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
@@ -218,7 +218,7 @@ class BuyingController(SubcontractingController):
|
||||
# set contact and address details for supplier, if they are not mentioned
|
||||
if getattr(self, "supplier", None):
|
||||
self.update_if_missing(
|
||||
_get_party_details(
|
||||
get_party_details(
|
||||
self.supplier,
|
||||
party_type="Supplier",
|
||||
doctype=self.doctype,
|
||||
|
||||
@@ -77,8 +77,7 @@
|
||||
"fieldname": "customer_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Email",
|
||||
"reqd": 1,
|
||||
"options": "Email"
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "linked_docs_section",
|
||||
@@ -103,7 +102,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-06-06 13:05:59.300573",
|
||||
"modified": "2024-03-27 13:05:59.300573",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Appointment",
|
||||
|
||||
@@ -611,9 +611,6 @@ erpnext.buying.get_items_from_product_bundle = function (frm) {
|
||||
fieldname: "product_bundle",
|
||||
options: "Product Bundle",
|
||||
reqd: 1,
|
||||
get_query: () => {
|
||||
return { filters: { disabled: 0 } };
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Currency",
|
||||
|
||||
@@ -53,7 +53,7 @@ class TestCustomer(ERPNextTestSuite):
|
||||
doc.delete()
|
||||
|
||||
def test_party_details(self):
|
||||
from erpnext.accounts.party import _get_party_details
|
||||
from erpnext.accounts.party import get_party_details
|
||||
|
||||
to_check = {
|
||||
"selling_price_list": None,
|
||||
@@ -75,7 +75,7 @@ class TestCustomer(ERPNextTestSuite):
|
||||
"Contact", "_Test Contact for _Test Customer-_Test Customer", "is_primary_contact", 1
|
||||
)
|
||||
|
||||
details = _get_party_details("_Test Customer")
|
||||
details = get_party_details("_Test Customer")
|
||||
|
||||
for key, value in to_check.items():
|
||||
val = details.get(key)
|
||||
@@ -85,10 +85,10 @@ class TestCustomer(ERPNextTestSuite):
|
||||
self.assertEqual(value, val)
|
||||
|
||||
def test_party_details_tax_category(self):
|
||||
from erpnext.accounts.party import _get_party_details
|
||||
from erpnext.accounts.party import get_party_details
|
||||
|
||||
# Tax Category without Address
|
||||
details = _get_party_details("_Test Customer With Tax Category")
|
||||
details = get_party_details("_Test Customer With Tax Category")
|
||||
self.assertEqual(details.tax_category, "_Test Tax Category 1")
|
||||
|
||||
frappe.get_doc(
|
||||
@@ -120,13 +120,13 @@ class TestCustomer(ERPNextTestSuite):
|
||||
# Tax Category from Billing Address
|
||||
settings.determine_address_tax_category_from = "Billing Address"
|
||||
settings.save()
|
||||
details = _get_party_details("_Test Customer With Tax Category")
|
||||
details = get_party_details("_Test Customer With Tax Category")
|
||||
self.assertEqual(details.tax_category, "_Test Tax Category 2")
|
||||
|
||||
# Tax Category from Shipping Address
|
||||
settings.determine_address_tax_category_from = "Shipping Address"
|
||||
settings.save()
|
||||
details = _get_party_details("_Test Customer With Tax Category")
|
||||
details = get_party_details("_Test Customer With Tax Category")
|
||||
self.assertEqual(details.tax_category, "_Test Tax Category 3")
|
||||
|
||||
# Rollback
|
||||
|
||||
@@ -65,12 +65,9 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "A disabled Product Bundle cannot be selected in transactions.",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Disabled",
|
||||
"no_copy": 1
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_eonk",
|
||||
@@ -80,7 +77,7 @@
|
||||
"icon": "fa fa-sitemap",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-10 16:00:00.000000",
|
||||
"modified": "2024-03-27 13:10:19.599302",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Product Bundle",
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.listview_settings["Product Bundle"] = {
|
||||
add_fields: ["disabled"],
|
||||
get_indicator(doc) {
|
||||
if (doc.disabled) {
|
||||
return [__("Disabled"), "grey", "disabled,=,1"];
|
||||
}
|
||||
return [__("Active"), "green", "disabled,=,0"];
|
||||
},
|
||||
};
|
||||
@@ -7,7 +7,7 @@ from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
|
||||
from erpnext import get_default_company
|
||||
from erpnext.accounts.party import _get_party_details
|
||||
from erpnext.accounts.party import get_party_details
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -125,7 +125,7 @@ def get_data(filters=None):
|
||||
|
||||
|
||||
def get_customer_details(filters):
|
||||
customer_details = _get_party_details(party=filters.get("customer"), party_type="Customer")
|
||||
customer_details = get_party_details(party=filters.get("customer"), party_type="Customer")
|
||||
customer_details.update(
|
||||
{"company": get_default_company(), "price_list": customer_details.get("selling_price_list")}
|
||||
)
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case, CustomFunction
|
||||
from frappe.query_builder.functions import Count, Max, Sum
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
@@ -26,69 +24,50 @@ def execute(filters=None):
|
||||
customers = get_sales_details(doctype)
|
||||
|
||||
data = []
|
||||
for row in customers:
|
||||
if cint(row[8]) >= cint(days_since_last_order):
|
||||
row.insert(7, get_last_sales_amt(row[0], doctype))
|
||||
data.append(row)
|
||||
for cust in customers:
|
||||
if cint(cust[8]) >= cint(days_since_last_order):
|
||||
cust.insert(7, get_last_sales_amt(cust[0], doctype))
|
||||
data.append(cust)
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_sales_details(doctype):
|
||||
customer = frappe.qb.DocType("Customer")
|
||||
sales_doctype = frappe.qb.DocType(doctype)
|
||||
|
||||
date_diff = CustomFunction("DATEDIFF", ["d1", "d2"])
|
||||
current_date = CustomFunction("CURRENT_DATE", [])
|
||||
|
||||
cond = """sum(so.base_net_total) as 'total_order_considered',
|
||||
max(so.posting_date) as 'last_order_date',
|
||||
DATEDIFF(CURRENT_DATE, max(so.posting_date)) as 'days_since_last_order' """
|
||||
if doctype == "Sales Order":
|
||||
total_considered = Sum(
|
||||
Case()
|
||||
.when(
|
||||
sales_doctype.status == "Stopped",
|
||||
sales_doctype.base_net_total * sales_doctype.per_delivered / 100,
|
||||
)
|
||||
.else_(sales_doctype.base_net_total)
|
||||
)
|
||||
date_col = sales_doctype.transaction_date
|
||||
else:
|
||||
total_considered = Sum(sales_doctype.base_net_total)
|
||||
date_col = sales_doctype.posting_date
|
||||
cond = """sum(if(so.status = "Stopped",
|
||||
so.base_net_total * so.per_delivered/100,
|
||||
so.base_net_total)) as 'total_order_considered',
|
||||
max(so.transaction_date) as 'last_order_date',
|
||||
DATEDIFF(CURRENT_DATE, max(so.transaction_date)) as 'days_since_last_order'"""
|
||||
|
||||
last_order_date = Max(date_col)
|
||||
days_since_last_order = date_diff(current_date(), last_order_date)
|
||||
|
||||
return (
|
||||
frappe.qb.from_(customer)
|
||||
.inner_join(sales_doctype)
|
||||
.on(customer.name == sales_doctype.customer)
|
||||
.select(
|
||||
customer.name,
|
||||
customer.customer_name,
|
||||
customer.territory,
|
||||
customer.customer_group,
|
||||
Count(sales_doctype.name).distinct().as_("num_of_order"),
|
||||
Sum(sales_doctype.base_net_total).as_("total_order_value"),
|
||||
total_considered.as_("total_order_considered"),
|
||||
last_order_date.as_("last_order_date"),
|
||||
days_since_last_order.as_("days_since_last_order"),
|
||||
)
|
||||
.where(sales_doctype.docstatus == 1)
|
||||
.groupby(customer.name)
|
||||
.orderby(days_since_last_order, order=frappe.qb.desc)
|
||||
).run(as_list=True)
|
||||
return frappe.db.sql(
|
||||
f"""select
|
||||
cust.name,
|
||||
cust.customer_name,
|
||||
cust.territory,
|
||||
cust.customer_group,
|
||||
count(distinct(so.name)) as 'num_of_order',
|
||||
sum(base_net_total) as 'total_order_value', {cond}
|
||||
from `tabCustomer` cust, `tab{doctype}` so
|
||||
where cust.name = so.customer and so.docstatus = 1
|
||||
group by cust.name
|
||||
order by 'days_since_last_order' desc """,
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
|
||||
def get_last_sales_amt(customer, doctype):
|
||||
sales_doctype = frappe.qb.DocType(doctype)
|
||||
date_col = sales_doctype.transaction_date if doctype == "Sales Order" else sales_doctype.posting_date
|
||||
|
||||
res = (
|
||||
frappe.qb.from_(sales_doctype)
|
||||
.select(sales_doctype.base_net_total)
|
||||
.where((sales_doctype.customer == customer) & (sales_doctype.docstatus == 1))
|
||||
.orderby(date_col, order=frappe.qb.desc)
|
||||
.limit(1)
|
||||
).run()
|
||||
cond = "posting_date"
|
||||
if doctype == "Sales Order":
|
||||
cond = "transaction_date"
|
||||
res = frappe.db.sql(
|
||||
f"""select base_net_total from `tab{doctype}`
|
||||
where customer = %s and docstatus = 1 order by {cond} desc
|
||||
limit 1""",
|
||||
customer,
|
||||
)
|
||||
|
||||
return res and res[0][0] or 0
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, getdate, today
|
||||
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.selling.report.inactive_customers.inactive_customers import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestInactiveCustomers(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.customer = frappe.get_doc(doctype="Customer", customer_name="_Test Inactive Customer").insert()
|
||||
self.last_order_date = add_days(today(), -120)
|
||||
so = make_sales_order(
|
||||
customer=self.customer.name,
|
||||
transaction_date=self.last_order_date,
|
||||
qty=5,
|
||||
rate=200,
|
||||
)
|
||||
so.submit()
|
||||
self.sales_order = so
|
||||
|
||||
def test_invalid_doctype_is_rejected(self):
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
execute,
|
||||
{"doctype": "Purchase Order", "days_since_last_order": 30},
|
||||
)
|
||||
|
||||
def test_inactive_customer_is_listed_with_expected_columns(self):
|
||||
columns, data = execute({"doctype": "Sales Order", "days_since_last_order": 30})
|
||||
|
||||
row = self.get_customer_row(data)
|
||||
self.assertIsNotNone(row, "Inactive customer should be present in the report")
|
||||
|
||||
# Column contract: the report relies on positional access.
|
||||
self.assertEqual(row[0], self.customer.name)
|
||||
self.assertEqual(row[7], 1000) # Last Order Amount inserted at index 7 (5 * 200)
|
||||
self.assertEqual(getdate(row[8]), getdate(self.last_order_date)) # Last Order Date
|
||||
self.assertGreaterEqual(row[9], 30) # Days Since Last Order
|
||||
|
||||
def test_recent_customer_is_excluded(self):
|
||||
_columns, data = execute({"doctype": "Sales Order", "days_since_last_order": 200})
|
||||
self.assertIsNone(
|
||||
self.get_customer_row(data),
|
||||
"Customer ordering within the threshold must be excluded",
|
||||
)
|
||||
|
||||
def get_customer_row(self, data):
|
||||
return next((row for row in data if row[0] == self.customer.name), None)
|
||||
@@ -13,8 +13,6 @@ def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
entries = get_entries(filters)
|
||||
item_details = get_item_details()
|
||||
@@ -51,17 +49,10 @@ def execute(filters=None):
|
||||
return columns, data
|
||||
|
||||
|
||||
def validate_filters(filters):
|
||||
ALLOWED_DOCTYPES = ["Sales Order", "Sales Invoice", "Delivery Note"]
|
||||
|
||||
def get_columns(filters):
|
||||
if not filters.get("doc_type"):
|
||||
msgprint(_("Please select the document type first"), raise_exception=1)
|
||||
|
||||
if filters.get("doc_type") not in ALLOWED_DOCTYPES:
|
||||
frappe.throw(_("{0}, {1} or {2} are the only allowed options.").format(*ALLOWED_DOCTYPES))
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
columns = [
|
||||
{
|
||||
"label": _(filters["doc_type"]),
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"icon": "icon-legal",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-06 16:35:34.394675",
|
||||
"modified": "2026-04-29 22:51:49.285298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Terms and Conditions",
|
||||
@@ -135,7 +135,7 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
@@ -152,15 +152,6 @@
|
||||
"read": 1,
|
||||
"role": "HR Manager",
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -294,7 +294,7 @@ def get_product_bundle_component_rows(item):
|
||||
def get_product_bundle_parent_rows(item):
|
||||
rows = frappe.get_all(
|
||||
"Product Bundle",
|
||||
filters={"new_item_code": item, "docstatus": 0},
|
||||
filters={"new_item_code": item, "disabled": 0, "docstatus": 0},
|
||||
fields=["name", "new_item_code", "disabled"],
|
||||
order_by="name asc",
|
||||
)
|
||||
@@ -463,7 +463,7 @@ def get_product_bundle_map(bundle_names):
|
||||
row.name: row
|
||||
for row in frappe.get_all(
|
||||
"Product Bundle",
|
||||
filters={"name": ["in", bundle_names], "docstatus": 0},
|
||||
filters={"name": ["in", bundle_names], "disabled": 0, "docstatus": 0},
|
||||
fields=["name", "new_item_code", "disabled"],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ def get_items(filters):
|
||||
item.brand,
|
||||
item.stock_uom,
|
||||
)
|
||||
.where((IfNull(item.disabled, 0) == 0) & (IfNull(pb.disabled, 0) == 0))
|
||||
.where(IfNull(item.disabled, 0) == 0)
|
||||
)
|
||||
|
||||
if item_code := filters.get("item_code"):
|
||||
|
||||
@@ -306,11 +306,6 @@ class FIFOSlots:
|
||||
# prepare single sle voucher detail lookup
|
||||
self.prepare_stock_reco_voucher_wise_count()
|
||||
|
||||
if stock_ledger_entries is None:
|
||||
# nested queries invalidate the streaming cursor below,
|
||||
# so batchwise valuation flags must be resolved beforehand
|
||||
self._prefetch_batchwise_valuations()
|
||||
|
||||
with frappe.db.unbuffered_cursor():
|
||||
if stock_ledger_entries is None:
|
||||
stock_ledger_entries = self._get_stock_ledger_entries()
|
||||
@@ -428,38 +423,12 @@ class FIFOSlots:
|
||||
|
||||
def _get_batchwise_valuation(self, batch_no: str):
|
||||
if batch_no not in self.batchwise_valuation_by_batch:
|
||||
# only reachable when stock ledger entries are passed in directly;
|
||||
# the streaming path prefetches all flags before iteration
|
||||
self.batchwise_valuation_by_batch[batch_no] = frappe.db.get_value(
|
||||
"Batch", batch_no, "use_batchwise_valuation"
|
||||
)
|
||||
|
||||
return self.batchwise_valuation_by_batch[batch_no]
|
||||
|
||||
def _prefetch_batchwise_valuations(self) -> None:
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
to_date = get_datetime(self.filters.get("to_date") + " 23:59:59")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
.left_join(batch)
|
||||
.on(sle.batch_no == batch.name)
|
||||
.select(sle.batch_no, batch.use_batchwise_valuation)
|
||||
.distinct()
|
||||
.where(
|
||||
(sle.batch_no.isnotnull())
|
||||
& (sle.company == self.filters.get("company"))
|
||||
& (sle.posting_datetime <= to_date)
|
||||
& (sle.is_cancelled != 1)
|
||||
)
|
||||
)
|
||||
|
||||
query = self._apply_filter(query, sle, "item_code")
|
||||
|
||||
for batch_no, use_batchwise_valuation in query.run():
|
||||
self.batchwise_valuation_by_batch[batch_no] = use_batchwise_valuation
|
||||
|
||||
def _init_key_stores(self, row: dict) -> tuple:
|
||||
"Initialise keys and FIFO Queue."
|
||||
|
||||
|
||||
@@ -1434,80 +1434,6 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
item_result["fifo_queue"], [[batch_no.upper(), 1, 5.0, getdate(add_days(base_date, -2)), 50.0]]
|
||||
)
|
||||
|
||||
def test_legacy_batch_no_sle_with_streaming_cursor(self):
|
||||
"""SLEs carrying the legacy batch_no field must not trigger nested
|
||||
queries while entries stream through an unbuffered cursor."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_batch_from_bundle,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
|
||||
suffix = frappe.generate_hash(length=8).upper()
|
||||
item_code = make_item(
|
||||
f"Test Stock Ageing Legacy Batch {suffix}",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": f"SA-LEG-{suffix}-.###",
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
base_date = nowdate()
|
||||
|
||||
reco = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
qty=10,
|
||||
rate=10,
|
||||
posting_date=add_days(base_date, -2),
|
||||
posting_time="10:00:00",
|
||||
)
|
||||
batch_no = get_batch_from_bundle(reco.items[0].serial_and_batch_bundle)
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
|
||||
create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
qty=5,
|
||||
rate=10,
|
||||
batch_no=batch_no,
|
||||
posting_date=add_days(base_date, -1),
|
||||
posting_time="10:00:00",
|
||||
)
|
||||
|
||||
# mimic pre-bundle data where SLEs carry batch_no directly
|
||||
frappe.db.set_value(
|
||||
"Stock Ledger Entry",
|
||||
{"item_code": item_code},
|
||||
"batch_no",
|
||||
batch_no,
|
||||
)
|
||||
|
||||
filters = frappe._dict(
|
||||
company="_Test Company",
|
||||
to_date=base_date,
|
||||
ranges=["30", "60", "90"],
|
||||
item_code=item_code,
|
||||
)
|
||||
fifo_slots = FIFOSlots(filters)
|
||||
|
||||
# fetch row by row so the streaming result set is still active
|
||||
# while each stock ledger entry is processed
|
||||
with patch("frappe.database.database.SQL_ITERATOR_BATCH_SIZE", 1):
|
||||
slots = fifo_slots.generate()
|
||||
|
||||
self.assertEqual(fifo_slots.batchwise_valuation_by_batch.get(batch_no), 1)
|
||||
self.assertEqual(slots[item_code]["total_qty"], 5.0)
|
||||
|
||||
|
||||
def generate_item_and_item_wh_wise_slots(filters, sle):
|
||||
"Return results with and without 'show_warehouse_wise_stock'"
|
||||
|
||||
Reference in New Issue
Block a user