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

This commit is contained in:
diptanilsaha
2026-04-29 02:28:40 +05:30
committed by GitHub
34 changed files with 810 additions and 140 deletions

View File

@@ -2306,22 +2306,20 @@ def get_outstanding_reference_documents(args, validate=False):
# Get positive outstanding sales /purchase invoices
condition = ""
if args.get("voucher_type") and args.get("voucher_no"):
condition = " and voucher_type={} and voucher_no={}".format(
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
)
condition = f" and voucher_type={frappe.db.escape(args['voucher_type'])} and voucher_no={frappe.db.escape(args['voucher_no'])}"
common_filter.append(ple.voucher_type == args["voucher_type"])
common_filter.append(ple.voucher_no == args["voucher_no"])
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
condition += f" and cost_center={frappe.db.escape(args.get('cost_center'))}"
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if args.get(dim.fieldname):
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
condition += f" and {dim.fieldname}={frappe.db.escape(args.get(dim.fieldname))}"
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
date_fields_dict = {
@@ -2331,17 +2329,15 @@ def get_outstanding_reference_documents(args, validate=False):
for fieldname, date_fields in date_fields_dict.items():
if args.get(date_fields[0]) and args.get(date_fields[1]):
condition += " and {} between '{}' and '{}'".format(
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
)
condition += f" and {fieldname} between {frappe.db.escape(args.get(date_fields[0]))} and {frappe.db.escape(args.get(date_fields[1]))}"
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
elif args.get(date_fields[0]):
# if only from date is supplied
condition += f" and {fieldname} >= '{args.get(date_fields[0])}'"
condition += f" and {fieldname} >= {frappe.db.escape(args.get(date_fields[0]))}"
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
elif args.get(date_fields[1]):
# if only to date is supplied
condition += f" and {fieldname} <= '{args.get(date_fields[1])}'"
condition += f" and {fieldname} <= {frappe.db.escape(args.get(date_fields[1]))}"
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
if args.get("company"):
@@ -2561,7 +2557,7 @@ def get_orders_to_be_billed(
active_dimensions = get_dimensions(True)[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
condition += f" and {dim.fieldname}={frappe.db.escape(filters.get(dim.fieldname))}"
if party_account_currency == company_currency:
grand_total_field = "base_grand_total"

View File

@@ -200,6 +200,30 @@ class TestPaymentEntry(FrappeTestCase):
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 100)
def test_reference_outstanding_amount_on_advance_pull(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
so = make_sales_order(qty=1, rate=1000)
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
pe.paid_amount = pe.received_amount = 500
pe.references[0].allocated_amount = 500
pe.insert()
pe.submit()
so.reload()
self.assertEqual(so.advance_paid, 500)
si = make_sales_invoice(so.name)
si.allocate_advances_automatically = 1
si.save()
self.assertEqual(si.get("advances")[0].allocated_amount, 500)
self.assertEqual(si.get("advances")[0].reference_name, pe.name)
si.submit()
pe.load_from_db()
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(pe.references[0].outstanding_amount, si.outstanding_amount)
def test_payment_entry_against_pi(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",
@@ -1937,6 +1961,37 @@ class TestPaymentEntry(FrappeTestCase):
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
def test_project_name_in_exchange_gain_loss_entry(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_submit=True,
)
from erpnext.projects.doctype.project.test_project import make_project
si.project = make_project({"project_name": "_Test Project for Exchange Gain Loss Entry"}).name
si.submit()
pe = get_payment_entry("Sales Invoice", si.name)
pe.source_exchange_rate = 100
pe.insert()
pe.submit()
rows = frappe.get_all(
"Journal Entry Account",
or_filters=[{"reference_name": pe.name}, {"reference_name": si.name}],
fields=["project"],
)
self.assertEqual(len(rows), 2)
self.assertEqual(rows[0].project, si.project)
self.assertEqual(rows[1].project, si.project)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -46,8 +46,8 @@ frappe.ui.form.on("Period Closing Voucher", {
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
from_date: frm.doc.period_start_date,
to_date: frm.doc.period_end_date,
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,

View File

@@ -34,6 +34,17 @@ frappe.query_reports["Accounts Payable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_account",
label: __("Payable Account"),

View File

@@ -120,3 +120,49 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 300.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project Output", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([pi.name, project.name, 300], [row.voucher_no, row.project, row.outstanding])

View File

@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Payable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -36,6 +36,17 @@ frappe.query_reports["Accounts Receivable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -194,6 +194,7 @@ class ReceivablePayableReport:
and ple.against_voucher_type in self.advance_payment_doctypes
):
self.voucher_balance[key].cost_center = ple.cost_center
self.voucher_balance[key].project = ple.project
self.get_invoices(ple)
@@ -360,6 +361,7 @@ class ReceivablePayableReport:
posting_date,
account_currency,
cost_center,
project,
sum(invoiced) `invoiced`,
sum(paid) `paid`,
sum(credit_note) `credit_note`,
@@ -388,6 +390,7 @@ class ReceivablePayableReport:
"credit_note_in_account_currency",
"outstanding_in_account_currency",
"cost_center",
"project",
]:
_d[field] = x.get(field)
@@ -925,6 +928,7 @@ class ReceivablePayableReport:
ple.against_voucher_no,
ple.party_type,
ple.cost_center,
ple.project,
ple.party,
ple.posting_date,
ple.due_date,
@@ -992,6 +996,9 @@ class ReceivablePayableReport:
if self.filters.cost_center:
self.get_cost_center_conditions()
if self.filters.project:
self.qb_selection_filter.append(self.ple.project.isin(self.filters.project))
self.add_accounting_dimensions_filters()
def get_cost_center_conditions(self):
@@ -1231,6 +1238,7 @@ class ReceivablePayableReport:
)
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Project"), fieldname="project", fieldtype="Link", options="Project")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
self.add_column(
label=_("Voucher No"),
@@ -1403,6 +1411,7 @@ class InitSQLProceduresForAR:
posting_date date,
account_currency {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
invoiced {_currency_type},
paid {_currency_type},
credit_note {_currency_type},
@@ -1422,6 +1431,7 @@ class InitSQLProceduresForAR:
against_voucher_no {_varchar_type},
party_type {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
party {_varchar_type},
posting_date date,
due_date date,
@@ -1450,7 +1460,7 @@ class InitSQLProceduresForAR:
begin
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
then
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, ple.project, 0, 0, 0, 0, 0, 0);
end if;
end;
"""
@@ -1492,7 +1502,7 @@ class InitSQLProceduresForAR:
end if;
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', '', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
end;
"""

View File

@@ -1204,3 +1204,52 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(len(report[1]), 2)
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 100.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project even when the payment entry
has no project set.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project Output", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
# payment has no project — report row must still show the invoice's project
self.create_payment_entry(si.name)
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])

View File

@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Receivable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -501,7 +501,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, inc
else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount
from `tabPurchase Taxes and Charges`
where parent in (%s) and category in ('Total', 'Valuation and Total')
and base_tax_amount_after_discount_amount != 0
and base_tax_amount_after_discount_amount != 0 and parenttype='Purchase Invoice'
group by parent, account_head, add_deduct_tax
"""
% ", ".join(["%s"] * len(invoice_list)),

View File

@@ -6,6 +6,7 @@ from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months, today
from erpnext.accounts.report.purchase_register.purchase_register import execute
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
class TestPurchaseRegister(FrappeTestCase):
@@ -26,6 +27,52 @@ class TestPurchaseRegister(FrappeTestCase):
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ignores_tax_rows_from_other_doctype(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
pi = make_purchase_invoice()
# Real workflow setup: create a Purchase Receipt tax row in the same shared child table.
pr = make_purchase_receipt(
company="_Test Company 6",
supplier="_Test Supplier",
item="_Test Item",
warehouse="_Test Warehouse - _TC6",
cost_center="_Test Cost Center - _TC6",
do_not_save=1,
do_not_submit=1,
qty=1,
rate=1000,
)
pr.append(
"taxes",
{
"account_head": "GST - _TC6",
"cost_center": "_Test Cost Center - _TC6",
"add_deduct_tax": "Add",
"category": "Valuation and Total",
"charge_type": "Actual",
"description": "PR Tax",
"tax_amount": 100.0,
"rate": 100,
},
)
pr.insert()
pr.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Purchase Receipt", pr.name, pi.name, force=True)
report_results = execute(filters)
first_row = frappe._dict(report_results[1][0])
self.assertEqual(first_row.voucher_no, pi.name)
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ledger_view(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")

View File

@@ -5,6 +5,7 @@ from frappe.utils import getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.sales_register.sales_register import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
@@ -75,6 +76,43 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
report_output = {k: v for k, v in res[0].items() if k in expected_result}
self.assertDictEqual(report_output, expected_result)
def test_sales_register_ignores_tax_rows_from_other_doctype(self):
si = self.create_sales_invoice(rate=98)
# Real workflow setup: create a Sales Order with taxes in the shared child table.
so = make_sales_order(
item=self.item,
company=self.company,
customer=self.customer,
rate=77,
do_not_save=1,
do_not_submit=1,
)
so.append(
"taxes",
{
"charge_type": "Actual",
"account_head": self.income_account,
"description": "SO Tax",
"tax_amount": 55.0,
},
)
so.insert()
so.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Sales Order", so.name, si.name, force=True)
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
report = execute(filters)
res = [x for x in report[1] if x.get("voucher_no") == si.name]
self.assertEqual(len(res), 1)
result = frappe._dict(res[0])
self.assertEqual(result.net_total, 98.0)
self.assertEqual(result.tax_total, 0)
self.assertEqual(result.grand_total, 98.0)
def test_journal_with_cost_center_filter(self):
je1 = frappe.get_doc(
{

View File

@@ -500,7 +500,7 @@ def reconcile_against_document(
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
dimensions_dict=dimensions_dict,
)
if referenced_row.get("outstanding_amount"):
if referenced_row.get("outstanding_amount") and entry.get("outstanding_amount") is None:
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
reposting_rows.append(referenced_row)
@@ -2320,6 +2320,7 @@ def create_gain_loss_journal(
ref2_detail_no,
cost_center,
dimensions,
project=None,
) -> str:
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
@@ -2346,6 +2347,7 @@ def create_gain_loss_journal(
"account_currency": party_account_currency,
"exchange_rate": 0,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref1_dt,
"reference_name": ref1_dn,
"reference_detail_no": ref1_detail_no,
@@ -2363,6 +2365,7 @@ def create_gain_loss_journal(
"account_currency": gain_loss_account_currency,
"exchange_rate": 1,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref2_dt,
"reference_name": ref2_dn,
"reference_detail_no": ref2_detail_no,

View File

@@ -1751,6 +1751,7 @@ class AccountsController(TransactionBase):
arg.get("referenced_row"),
arg.get("cost_center"),
dimensions_dict,
arg.get("project"),
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -1835,6 +1836,7 @@ class AccountsController(TransactionBase):
d.idx,
self.cost_center,
dimensions_dict,
self.project,
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(

View File

@@ -364,7 +364,17 @@ class BuyingController(SubcontractingController):
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
)
net_rate = item.qty * item.base_net_rate
net_rate = (
flt(
(item.base_net_amount / item.received_qty) * item.qty,
item.precision("base_net_amount"),
)
if item.received_qty
and frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
else item.base_net_amount
)
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate

View File

@@ -10,6 +10,7 @@ erpnext.edi.import_genericode = function (listview_or_form) {
method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode",
doctype: doctype,
docname: docname,
allow_web_link: false,
allow_toggle_private: false,
allow_take_photo: false,
on_success: function (_file_doc, r) {

View File

@@ -1,48 +1,118 @@
import json
from urllib.parse import urlsplit
import frappe
import requests
from frappe import _
from frappe.utils import escape_html
from frappe.utils.file_manager import save_file
from lxml import etree
URL_PREFIXES = ("http://", "https://")
GENERICODE_FETCH_TIMEOUT = 15
LOCAL_FILE_PREFIXES = ("/files/", "/private/files/")
class RemoteGenericodeUrlNotAllowedError(Exception):
pass
class CodeListSelectionMismatchError(Exception):
pass
@frappe.whitelist()
def import_genericode():
doctype = "Code List"
docname = frappe.form_dict.docname
content = frappe.local.uploaded_file
# recover the content, if it's a link
if (file_url := frappe.local.uploaded_file_url) and file_url.startswith(URL_PREFIXES):
try:
# If it's a URL, fetch the content and make it a local file (for durable audit)
response = requests.get(frappe.local.uploaded_file_url)
response.raise_for_status()
frappe.local.uploaded_file = content = response.content
frappe.local.uploaded_filename = frappe.local.uploaded_file_url.split("/")[-1]
frappe.local.uploaded_file_url = None
except Exception as e:
frappe.throw(f"<pre>{e!s}</pre>", title=_("Fetching Error"))
if file_url := frappe.local.uploaded_file_url:
file_path = frappe.utils.file_manager.get_file_path(file_url)
with open(file_path.encode(), mode="rb") as f:
content = f.read()
# Parse the xml content
parser = etree.XMLParser(
remove_blank_text=True,
resolve_entities=False,
load_dtd=False,
no_network=True,
)
try:
root = etree.fromstring(content, parser=parser)
except Exception as e:
frappe.throw(f"<pre>{e!s}</pre>", title=_("Parsing Error"))
content, file_name = get_uploaded_genericode_file()
return import_genericode_content(
doctype="Code List",
docname=frappe.form_dict.docname,
content=content,
file_name=file_name,
)
except RemoteGenericodeUrlNotAllowedError:
frappe.throw(
_("Importing Code Lists from remote URLs is not allowed."),
title=_("Invalid Upload"),
)
except CodeListSelectionMismatchError:
frappe.throw(_("The uploaded file does not match the selected Code List."))
except etree.XMLSyntaxError:
frappe.throw(
_("The uploaded file could not be parsed as a genericode XML document."),
title=_("Parsing Error"),
)
def import_genericode_from_url(
url: str,
doctype: str = "Code List",
docname: str | None = None,
):
"""Import a Code List from a trusted backend URL."""
content = fetch_genericode_from_url(url)
file_name = urlsplit(url).path.rsplit("/", 1)[-1] or "genericode.xml"
return import_genericode_content(
doctype=doctype,
docname=docname,
content=content,
file_name=file_name,
)
def get_uploaded_genericode_file() -> tuple[bytes, str | None]:
uploaded_data = frappe.local.uploaded_file
file_name = frappe.local.uploaded_filename
if uploaded_data and file_name:
return uploaded_data, file_name
file_url = frappe.local.uploaded_file_url
if not file_url:
raise frappe.ValidationError(_("No file uploaded or URL provided."))
if not is_local_file_url(file_url):
raise RemoteGenericodeUrlNotAllowedError
file_doc = frappe.get_doc("File", {"file_url": file_url})
file_doc.check_permission("read")
return read_file_bytes(file_doc), file_name
def read_file_bytes(file_doc) -> bytes:
"""Return the raw bytes of a File document.
v15's `File.get_content` eagerly decodes to utf-8 and returns `str` for text
files, but `lxml.etree.fromstring` needs bytes when the XML declares an encoding.
"""
content = file_doc.get_content()
if isinstance(content, str):
content = content.encode("utf-8")
return content
def is_local_file_url(file_url: str | None) -> bool:
if not file_url:
return False
parsed = urlsplit(file_url.strip())
return not parsed.scheme and not parsed.netloc and parsed.path.startswith(LOCAL_FILE_PREFIXES)
def fetch_genericode_from_url(url: str) -> bytes:
response = requests.get(url, timeout=GENERICODE_FETCH_TIMEOUT)
response.raise_for_status()
return response.content
def import_genericode_content(
doctype: str,
docname: str | None,
content: bytes,
file_name: str | None,
):
root = parse_genericode_content(content)
# Extract the name (CanonicalVersionUri) from the parsed XML
name = root.find(".//CanonicalVersionUri").text
@@ -51,7 +121,7 @@ def import_genericode():
if frappe.db.exists(doctype, docname):
code_list = frappe.get_doc(doctype, docname)
if code_list.name != name:
frappe.throw(_("The uploaded file does not match the selected Code List."))
raise CodeListSelectionMismatchError
else:
# Create a new Code List document with the extracted name
code_list = frappe.new_doc(doctype)
@@ -60,19 +130,13 @@ def import_genericode():
code_list.from_genericode(root)
code_list.save()
# Attach the file and provide a recoverable identifier
file_doc = frappe.get_doc(
{
"doctype": "File",
"attached_to_doctype": "Code List",
"attached_to_name": code_list.name,
"folder": frappe.db.get_value("File", {"is_attachments_folder": 1}),
"file_name": frappe.local.uploaded_filename,
"file_url": frappe.local.uploaded_file_url,
"is_private": 1,
"content": content,
}
).save()
file_doc = save_file(
fname=file_name,
content=content,
dt=doctype,
dn=code_list.name,
is_private=1,
)
# Get available columns and example values
columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
@@ -87,6 +151,16 @@ def import_genericode():
}
def parse_genericode_content(content: bytes):
parser = etree.XMLParser(
remove_blank_text=True,
resolve_entities=False,
load_dtd=False,
no_network=True,
)
return etree.fromstring(content, parser=parser)
@frappe.whitelist()
def process_genericode_import(
code_list_name: str,

View File

@@ -0,0 +1,200 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from unittest.mock import Mock, patch
import frappe
import requests
from frappe.tests.utils import FrappeTestCase
from erpnext.edi.doctype.code_list import code_list_import
SAMPLE_GENERICODE = b"""<?xml version="1.0" encoding="UTF-8"?>
<CodeList>
<Identification>
<ShortName>Test Code List</ShortName>
<Version>1.0</Version>
<CanonicalUri>test-code-list</CanonicalUri>
<LongName>Code list for tests</LongName>
<Agency>
<ShortName>Test Agency</ShortName>
<Identifier>TEST</Identifier>
</Agency>
<LocationUri>https://example.com/codelists/test.xml</LocationUri>
</Identification>
<CanonicalVersionUri>test-code-list-v1</CanonicalVersionUri>
<ColumnSet>
<Column Id="code" />
<Column Id="name" />
<Column Id="category" />
</ColumnSet>
<SimpleCodeList>
<Row>
<Value ColumnRef="code"><SimpleValue>A</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Alpha</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 1</SimpleValue></Value>
</Row>
<Row>
<Value ColumnRef="code"><SimpleValue>B</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Beta</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 2</SimpleValue></Value>
</Row>
<Row>
<Value ColumnRef="code"><SimpleValue>C</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Gamma</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 1</SimpleValue></Value>
</Row>
</SimpleCodeList>
</CodeList>
"""
class TestCodeListImport(FrappeTestCase):
def test_import_genericode_rejects_remote_file_url(self):
self.set_upload_context(
file_name="trusted.xml",
file_url="https://example.com/codelists/trusted.xml",
)
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
with self.assertRaisesRegex(
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
):
code_list_import.import_genericode()
mock_get.assert_not_called()
def test_import_genericode_rejects_file_scheme_url(self):
self.set_upload_context(
file_name="trusted.xml",
file_url="file:///tmp/trusted.xml",
)
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
with self.assertRaisesRegex(
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
):
code_list_import.import_genericode()
mock_get.assert_not_called()
def test_import_genericode_from_trusted_url(self):
response = Mock()
response.content = SAMPLE_GENERICODE
response.raise_for_status.return_value = None
with patch(
"erpnext.edi.doctype.code_list.code_list_import.requests.get",
return_value=response,
) as mock_get:
import_result = code_list_import.import_genericode_from_url(
"https://example.com/codelists/trusted.xml"
)
self.assert_import_response(import_result)
mock_get.assert_called_once_with(
"https://example.com/codelists/trusted.xml",
timeout=code_list_import.GENERICODE_FETCH_TIMEOUT,
)
file_doc = frappe.get_doc("File", import_result["file"])
self.assertEqual(code_list_import.read_file_bytes(file_doc), SAMPLE_GENERICODE)
self.assertFalse(file_doc.file_url.startswith("https://"))
def test_import_genericode_from_trusted_url_propagates_fetch_errors(self):
with patch(
"erpnext.edi.doctype.code_list.code_list_import.requests.get",
side_effect=requests.Timeout,
):
with self.assertRaises(requests.Timeout):
code_list_import.import_genericode_from_url("https://example.com/codelists/trusted.xml")
def test_import_genericode_from_uploaded_file_returns_metadata(self):
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")
import_result = code_list_import.import_genericode()
self.assert_import_response(import_result)
file_doc = frappe.get_doc("File", import_result["file"])
self.assertEqual(code_list_import.read_file_bytes(file_doc), SAMPLE_GENERICODE)
def test_process_genericode_import_reads_file_doc_content(self):
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")
import_result = code_list_import.import_genericode()
count = code_list_import.process_genericode_import(
code_list_name=import_result["code_list"],
file_name=import_result["file"],
code_column="code",
title_column="name",
)
self.assertEqual(count, 3)
self.assertEqual(frappe.db.count("Common Code", {"code_list": import_result["code_list"]}), 3)
self.assertEqual(
frappe.db.get_value(
"Common Code",
{"code_list": import_result["code_list"], "common_code": "A"},
"title",
),
"Alpha",
)
def test_import_genericode_from_local_file_url(self):
source_file = frappe.get_doc(
{
"doctype": "File",
"file_name": "library_genericode.xml",
"content": SAMPLE_GENERICODE,
"is_private": 1,
}
).insert()
self.set_upload_context(file_name=source_file.file_name, file_url=source_file.file_url)
import_result = code_list_import.import_genericode()
self.assert_import_response(import_result)
def set_upload_context(
self,
content: bytes | None = None,
file_name: str = "genericode.xml",
file_url: str | None = None,
docname: str | None = None,
):
attrs = ("form_dict", "uploaded_file", "uploaded_file_url", "uploaded_filename")
originals = {attr: getattr(frappe.local, attr, None) for attr in attrs}
frappe.local.form_dict = frappe._dict(doctype="Code List", docname=docname)
frappe.local.uploaded_file = content
frappe.local.uploaded_file_url = file_url
frappe.local.uploaded_filename = file_name
def restore():
for attr, value in originals.items():
setattr(frappe.local, attr, value)
self.addCleanup(restore)
def assert_import_response(self, import_result):
self.assertEqual(
set(import_result),
{
"code_list",
"code_list_title",
"file",
"columns",
"example_values",
"filterable_columns",
},
)
self.assertEqual(import_result["code_list"], "test-code-list-v1")
self.assertEqual(import_result["code_list_title"], "Test Code List")
self.assertEqual(import_result["columns"], ["code", "name", "category"])
self.assertEqual(import_result["example_values"]["code"], ["A", "B", "C"])
self.assertEqual(import_result["example_values"]["name"], ["Alpha", "Beta", "Gamma"])
self.assertEqual(import_result["example_values"]["category"], ["Group 1", "Group 2", "Group 1"])
self.assertCountEqual(import_result["filterable_columns"]["category"], ["Group 1", "Group 2"])
self.assertTrue(frappe.db.exists("Code List", import_result["code_list"]))
self.assertTrue(frappe.db.exists("File", import_result["file"]))

View File

@@ -9,6 +9,8 @@ from frappe.model.document import Document
from frappe.utils.data import get_link_to_form
from lxml import etree
from erpnext.edi.doctype.code_list.code_list_import import parse_genericode_content, read_file_bytes
class CommonCode(Document):
# begin: auto-generated types
@@ -86,15 +88,15 @@ def simple_hash(input_string, length=6):
def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None):
"""Import genericode file and create Common Code entries"""
file_path = frappe.utils.file_manager.get_file_path(file_name)
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser=parser)
root = tree.getroot()
file_doc = frappe.get_doc("File", file_name)
file_doc.check_permission("read")
root = parse_genericode_content(read_file_bytes(file_doc))
# Construct the XPath expression
xpath_expr = ".//SimpleCodeList/Row"
filter_conditions = [
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'"
for column_ref, value in (filters or {}).items()
]
if filter_conditions:
xpath_expr += "[" + " and ".join(filter_conditions) + "]"
@@ -102,7 +104,7 @@ def import_genericode(code_list: str, file_name: str, column_map: dict, filters:
elements = root.xpath(xpath_expr)
total_elements = len(elements)
for i, xml_element in enumerate(elements, start=1):
common_code: "CommonCode" = frappe.new_doc("Common Code")
common_code: CommonCode = frappe.new_doc("Common Code")
common_code.code_list = code_list
common_code.from_genericode(column_map, xml_element)
common_code.save()

View File

@@ -120,7 +120,7 @@ class BlanketOrder(Document):
def validate_item_qty(self):
for d in self.items:
if d.qty < 0:
if flt(d.qty) < 0:
frappe.throw(_("Row {0}: Quantity cannot be negative.").format(d.idx))

View File

@@ -228,6 +228,18 @@ class WorkOrder(Document):
if self.production_plan_sub_assembly_item:
return
production_item = self.production_item
if self.material_request_item and (
mr_plan_item := frappe.get_value(
"Material Request Item", self.material_request_item, "material_request_plan_item"
)
):
if main_item_code := frappe.get_value(
"Material Request Plan Item", mr_plan_item, "main_item_code"
):
production_item = main_item_code
if self.sales_order:
self.check_sales_order_on_hold_or_close()
@@ -248,8 +260,8 @@ class WorkOrder(Document):
& (SalesOrder.docstatus == 1)
& (SalesOrder.name == self.sales_order)
& (
(SalesOrderItem.item_code == self.production_item)
| (ProductBundleItem.item_code == self.production_item)
(SalesOrderItem.item_code == production_item)
| (ProductBundleItem.item_code == production_item)
)
)
.run(as_dict=1)
@@ -268,7 +280,7 @@ class WorkOrder(Document):
& (SalesOrder.skip_delivery_note == 0)
& (SalesOrderItem.item_code == PackedItem.parent_item)
& (SalesOrder.docstatus == 1)
& (PackedItem.item_code == self.production_item)
& (PackedItem.item_code == production_item)
)
.run(as_dict=1)
)

View File

@@ -10,7 +10,18 @@ def execute():
)
if data:
frappe.db.auto_commit_on_many_writes = 1
frappe.db.bulk_update(
"Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data}
)
frappe.db.auto_commit_on_many_writes = 0
try:
frappe.db.bulk_update(
"Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data}
)
quotations = frappe.get_all(
"Quotation Item",
filters={"name": ["in", [d.quotation_item for d in data]]},
pluck="parent",
distinct=True,
)
for quotation in quotations:
doc = frappe.get_doc("Quotation", quotation)
doc.set_status(update=True, update_modified=False)
finally:
frappe.db.auto_commit_on_many_writes = 0

View File

@@ -0,0 +1,4 @@
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ pincode }} {{ city | upper }}<br>
{{ country | upper }}

View File

@@ -0,0 +1,4 @@
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ pincode }} {{ city | upper }}<br>
{{ country | upper }}

View File

@@ -123,6 +123,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
) {
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
this.frm.add_custom_button(__("Update Items"), () => {
erpnext.utils.update_child_items({
frm: this.frm,
@@ -137,8 +138,6 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
this.frm.trigger("set_as_lost_dialog");
});
}
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
}
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Opportunity")) {

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import _, msgprint
from frappe.core.doctype.sms_settings.sms_settings import send_sms
from frappe.model.document import Document
from frappe.query_builder import functions as fn
from frappe.utils import cstr
@@ -41,73 +42,117 @@ class SMSCenter(Document):
@frappe.whitelist()
def create_receiver_list(self):
rec, where_clause = "", ""
if self.send_to == "All Customer Contact":
where_clause = " and dl.link_doctype = 'Customer'"
if self.customer:
where_clause += (
" and dl.link_name = '%s'" % self.customer.replace("'", "'")
or " and ifnull(dl.link_name, '') != ''"
)
if self.send_to == "All Supplier Contact":
where_clause = " and dl.link_doctype = 'Supplier'"
if self.supplier:
where_clause += (
" and dl.link_name = '%s'" % self.supplier.replace("'", "'")
or " and ifnull(dl.link_name, '') != ''"
)
if self.send_to == "All Sales Partner Contact":
where_clause = " and dl.link_doctype = 'Sales Partner'"
if self.sales_partner:
where_clause += (
"and dl.link_name = '%s'" % self.sales_partner.replace("'", "'")
or " and ifnull(dl.link_name, '') != ''"
)
query = None
if self.send_to == "":
return
if self.send_to in [
"All Contact",
"All Customer Contact",
"All Supplier Contact",
"All Sales Partner Contact",
]:
rec = frappe.db.sql(
"""select CONCAT(ifnull(c.first_name,''), ' ', ifnull(c.last_name,'')),
c.mobile_no from `tabContact` c, `tabDynamic Link` dl where ifnull(c.mobile_no,'')!='' and
c.docstatus != 2 and dl.parent = c.name%s"""
% where_clause
)
query = self.get_contact_query_for_all_contacts()
elif self.send_to == "All Lead (Open)":
rec = frappe.db.sql(
"""select lead_name, mobile_no from `tabLead` where
ifnull(mobile_no,'')!='' and docstatus != 2 and status='Open'"""
)
query = self.get_contact_query_for_all_open_leads()
elif self.send_to == "All Employee (Active)":
where_clause = (
self.department and " and department = '%s'" % self.department.replace("'", "'") or ""
)
where_clause += self.branch and " and branch = '%s'" % self.branch.replace("'", "'") or ""
rec = frappe.db.sql(
"""select employee_name, cell_number from
`tabEmployee` where status = 'Active' and docstatus < 2 and
ifnull(cell_number,'')!='' %s"""
% where_clause
)
query = self.get_contact_query_for_all_active_employee()
elif self.send_to == "All Sales Person":
rec = frappe.db.sql(
"""select sales_person_name,
tabEmployee.cell_number from `tabSales Person` left join tabEmployee
on `tabSales Person`.employee = tabEmployee.name
where ifnull(tabEmployee.cell_number,'')!=''"""
)
query = self.get_contact_query_for_all_sales_person()
rec = query.run(as_list=1)
rec_list = ""
for d in rec:
rec_list += d[0] + " - " + d[1] + "\n"
self.receiver_list = rec_list
def get_contact_query_for_all_contacts(self):
Contact = frappe.qb.DocType("Contact")
DynamicLink = frappe.qb.DocType("Dynamic Link")
query = (
frappe.qb.from_(Contact)
.join(DynamicLink)
.on(DynamicLink.parent == Contact.name)
.select(
fn.Concat(fn.IfNull(Contact.first_name, ""), " ", fn.IfNull(Contact.last_name, "")),
Contact.mobile_no,
)
.where((fn.IfNull(Contact.mobile_no, "") != "") & (Contact.docstatus != 2))
)
if self.send_to == "All Customer Contact":
query = query.where(DynamicLink.link_doctype == "Customer")
query = (
query.where(DynamicLink.link_name == self.customer)
if self.customer
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
)
elif self.send_to == "All Supplier Contact":
query = query.where(DynamicLink.link_doctype == "Supplier")
query = (
query.where(DynamicLink.link_name == self.supplier)
if self.supplier
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
)
elif self.send_to == "All Sales Partner Contact":
query = query.where(DynamicLink.link_doctype == "Sales Partner")
query = (
query.where(DynamicLink.link_name == self.sales_partner)
if self.sales_partner
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
)
return query
def get_contact_query_for_all_open_leads(self):
Lead = frappe.qb.DocType("Lead")
query = (
frappe.qb.from_(Lead)
.select(Lead.lead_name, Lead.mobile)
.where((fn.IfNull(Lead.mobile_no, "") != "") & (Lead.docstatus != 2) & (Lead.status == "Open"))
)
return query
def get_contact_query_for_all_active_employee(self):
Employee = frappe.qb.DocType("Employee")
query = (
frappe.qb.from_(Employee)
.select(Employee.employee_name, Employee.cell_number)
.where(
(Employee.status == "Active")
& (Employee.docstatus != 2)
& (fn.IfNull(Employee.cell_number, "") != "")
)
)
if self.department:
query = query.where(Employee.department == self.department)
if self.branch:
query = query.where(Employee.branch == self.branch)
return query
def get_contact_query_for_all_sales_person(self):
SalesPerson = frappe.qb.DocType("Sales Person")
Employee = frappe.qb.DocType("Employee")
query = (
frappe.qb.from_(SalesPerson)
.left_join(Employee)
.on(SalesPerson.employee == Employee.name)
.select(SalesPerson.sales_person_name, Employee.cell_number)
.where(fn.IfNull(Employee.cell_number, "") != "")
)
return query
def get_receiver_nos(self):
receiver_nos = []
if self.receiver_list:

View File

@@ -510,7 +510,14 @@ class PurchaseReceipt(BuyingController):
else flt(item.net_amount, item.precision("net_amount"))
)
outgoing_amount = item.qty * item.base_net_rate
outgoing_amount = (
flt((item.base_net_amount / item.received_qty) * item.qty, item.precision("base_net_amount"))
if item.received_qty
and frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
else item.base_net_amount
)
if self.is_internal_transfer() and item.valuation_rate:
outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse))
credit_amount = outgoing_amount

View File

@@ -394,7 +394,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
f"""
SELECT distinct item_code, item_name
FROM `tab{from_doctype}`
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
JOIN `tab{parent_doctype}` ON `tab{parent_doctype}`.name = `tab{from_doctype}`.parent
WHERE parent=%(parent)s and `tab{parent_doctype}`.docstatus < 2 and item_code like %(txt)s
{qi_condition} {cond} {mcond}
ORDER BY item_code limit {cint(page_len)} offset {cint(start)}
""",

View File

@@ -925,6 +925,7 @@ class SerialandBatchBundle(Document):
parent.voucher_type,
parent.voucher_no,
)
.distinct()
.where(
(child.parent != self.name)
& (parent.item_code == self.item_code)

View File

@@ -270,8 +270,7 @@
"oldfieldname": "transfer_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
"read_only": 1,
"reqd": 1
"read_only": 1
},
{
"default": "0",
@@ -617,7 +616,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-03-02 14:05:23.116017",
"modified": "2026-04-27 11:40:38.294196",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",

View File

@@ -612,5 +612,5 @@ class FIFOSlots:
sr_item = frappe.db.get_value(
"Stock Reconciliation Item", row.voucher_detail_no, ["current_qty", "qty"], as_dict=True
)
if sr_item.qty and sr_item.current_qty:
if sr_item and sr_item.qty and sr_item.current_qty:
self.stock_reco_voucher_wise_count[row.voucher_detail_no] = sr_item.current_qty

View File

@@ -947,6 +947,9 @@ class update_entries_after:
if not self.wh_data.qty_after_transaction:
self.wh_data.stock_value = 0.0
if sle.actual_qty < 0:
sle.incoming_rate = 0
stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
self.wh_data.prev_stock_value = self.wh_data.stock_value

View File

@@ -10,9 +10,8 @@ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime
import erpnext
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_available_serial_nos,
)
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 get_available_serial_nos
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
from erpnext.stock.valuation import FIFOValuation, LIFOValuation
@@ -124,11 +123,19 @@ def get_stock_balance(
}
extra_cond = ""
if inventory_dimensions_dict:
inventory_dimensions_fieldname = [d.get("fieldname") for d in get_inventory_dimensions()]
for field, value in inventory_dimensions_dict.items():
column = frappe.utils.sanitize_column(field)
if field not in inventory_dimensions_fieldname:
frappe.throw(
_("{0} is not a valid {1} fieldname.").format(
frappe.bold(field), frappe.bold("Inventory Dimension")
)
)
args[field] = value
extra_cond += f" and {column} = %({field})s"
extra_cond += f" and {field} = %({field})s"
last_entry = get_previous_sle(args, extra_cond=extra_cond)