Merge remote-tracking branch 'upstream/develop' into erpnext-refactoring

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py
#	erpnext/buying/doctype/purchase_order/purchase_order.py
#	erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
#	erpnext/controllers/accounts_controller.py
#	erpnext/selling/doctype/sales_order/sales_order.py
#	erpnext/selling/doctype/sales_order/test_sales_order.py
#	erpnext/stock/doctype/delivery_note/delivery_note.py
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
#	erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
This commit is contained in:
Nabin Hait
2026-06-03 13:23:03 +05:30
44 changed files with 1227 additions and 207 deletions

View File

@@ -156,7 +156,7 @@ class AccountsController(TransactionBase):
if not get_meta(self.doctype).has_field("outstanding_amount"):
return
if self.get("is_return") and self.return_against and not self.get("is_pos"):
if self.get("is_return") and self.return_against and not (self.get("is_pos") or self.get("is_paid")):
against_voucher_outstanding = frappe.get_value(
self.doctype, self.return_against, "outstanding_amount"
)

View File

@@ -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
@@ -213,7 +213,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,
@@ -683,19 +683,6 @@ class BuyingController(SubcontractingController):
)
)
def check_for_on_hold_or_closed_status(self, ref_doctype, ref_fieldname):
for d in self.get("items"):
if d.get(ref_fieldname):
status = frappe.db.get_value(ref_doctype, d.get(ref_fieldname), "status")
if status in ("Closed", "On Hold"):
frappe.throw(
_("{ref_doctype} {ref_name} is {status}.").format(
ref_doctype=frappe.bold(_(ref_doctype)),
ref_name=frappe.bold(d.get(ref_fieldname)),
status=frappe.bold(_(status)),
)
)
def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
self.update_ordered_and_reserved_qty()

View File

@@ -469,11 +469,9 @@ class SellingController(StockController):
return so_qty, so_warehouse
def check_sales_order_on_hold_or_close(self, ref_fieldname):
for d in self.get("items"):
if d.get(ref_fieldname):
status = frappe.db.get_value("Sales Order", d.get(ref_fieldname), "status")
if status in ("Closed", "On Hold") and not self.is_return:
frappe.throw(_("Sales Order {0} is {1}").format(d.get(ref_fieldname), status))
if self.is_return:
return
self.check_for_on_hold_or_closed_status("Sales Order", ref_fieldname)
def update_reserved_qty(self):
so_map = {}

View File

@@ -524,9 +524,9 @@ class StatusUpdater(Document):
for args in self.status_updater:
# condition to include current record (if submit or no if cancel)
if self.docstatus == 1:
args["cond"] = " or parent='%s'" % self.name.replace('"', '"')
args["cond"] = " or parent=%s" % frappe.db.escape(self.name)
else:
args["cond"] = " and parent!='%s'" % self.name.replace('"', '"')
args["cond"] = " and parent!=%s" % frappe.db.escape(self.name)
self._update_children(args, update_modified)
@@ -556,9 +556,10 @@ class StatusUpdater(Document):
args["second_source_condition"] = frappe.db.sql(
""" select ifnull((select sum({second_source_field})
from `tab{second_source_dt}`
where `{second_join_field}`='{detail_id}'
where `{second_join_field}`=%(detail_id)s
and (`tab{second_source_dt}`.docstatus=1)
{second_source_extra_cond}), 0) """.format(**args)
{second_source_extra_cond}), 0) """.format(**args),
{"detail_id": args["detail_id"]},
)[0][0]
if args["detail_id"]:
@@ -569,9 +570,10 @@ class StatusUpdater(Document):
frappe.db.sql(
"""
(select ifnull(sum({source_field}), 0)
from `tab{source_dt}` where `{join_field}`='{detail_id}'
from `tab{source_dt}` where `{join_field}`=%(detail_id)s
and (docstatus=1 {cond}) {extra_cond})
""".format(**args)
""".format(**args),
{"detail_id": args["detail_id"]},
)[0][0]
or 0.0
)
@@ -582,7 +584,8 @@ class StatusUpdater(Document):
frappe.db.sql(
"""update `tab{target_dt}`
set {target_field} = {source_dt_value} {update_modified}
where name='{detail_id}'""".format(**args)
where name=%(detail_id)s""".format(**args),
{"detail_id": args["detail_id"]},
)
@staticmethod

View File

@@ -1803,6 +1803,43 @@ class StockController(AccountsController):
qty -= working_qty
def check_for_on_hold_or_closed_status(
self, ref_doctype: str, ref_fieldname: str, exclude_if_field: str | None = None
) -> None:
def _include(d):
return d.get(ref_fieldname) and not (exclude_if_field and d.get(exclude_if_field))
included = [(d, d.get(ref_fieldname)) for d in self.get("items") if _include(d)]
if not included:
return
status_map = {
r.name: r.status
for r in frappe.get_all(
ref_doctype,
filters={"name": ["in", {name for _, name in included}]},
fields=["name", "status"],
)
}
errors = []
seen = set()
for _d, ref_name in included:
if ref_name in seen:
continue
seen.add(ref_name)
if (status := status_map.get(ref_name)) in ("Closed", "On Hold"):
errors.append(
_("{ref_doctype} {ref_name} status is {status}.").format(
ref_doctype=frappe.bold(_(ref_doctype)),
ref_name=frappe.bold(ref_name),
status=frappe.bold(_(status)),
)
)
if errors:
frappe.throw("<br>".join(errors), frappe.InvalidStatusError)
@frappe.whitelist()
def show_accounting_ledger_preview(company: str, doctype: str, docname: str):

View File

@@ -304,6 +304,7 @@ class calculate_taxes_and_totals:
return
for item in self.doc.items:
item._unrounded_net_amount = None
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
cumulated_tax_fraction = 0
total_inclusive_tax_amount_per_qty = 0
@@ -331,7 +332,8 @@ class calculate_taxes_and_totals:
):
amount = flt(item.amount) - total_inclusive_tax_amount_per_qty
item.net_amount = flt(amount / (1 + cumulated_tax_fraction), item.precision("net_amount"))
item._unrounded_net_amount = amount / (1 + cumulated_tax_fraction)
item.net_amount = flt(item._unrounded_net_amount, item.precision("net_amount"))
item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate"))
item.discount_percentage = flt(
item.discount_percentage, item.precision("discount_percentage")
@@ -541,7 +543,9 @@ class calculate_taxes_and_totals:
actual_breakup = tax._total_tax_breakup
diff = flt(expected_amount - actual_breakup, 5)
if abs(diff) <= 0.5:
# TODO: fix rounding difference issues
# Allow up to 1 for zero-precision currencies (e.g. JPY, KRW)
if abs(diff) <= (1 if tax.precision("tax_amount") == 0 else 0.5):
detail_row = self.doc._item_wise_tax_details[last_idx]
detail_row["amount"] = flt(detail_row["amount"] + diff, 5)
@@ -600,7 +604,16 @@ class calculate_taxes_and_totals:
elif tax.charge_type == "On Net Total":
if tax.account_head in item_tax_map:
current_net_amount = item.net_amount
current_tax_amount = (tax_rate / 100.0) * item.net_amount
# Use unrounded net for inclusive taxes to avoid double rounding
if (
cint(tax.included_in_print_rate)
and not self.discount_amount_applied
and item._unrounded_net_amount is not None
):
current_tax_amount = (tax_rate / 100.0) * item._unrounded_net_amount
else:
current_tax_amount = (tax_rate / 100.0) * item.net_amount
elif tax.charge_type == "On Previous Row Amount":
current_net_amount = self.doc.get("taxes")[cint(tax.row_id) - 1].tax_amount_for_current_item
current_tax_amount = (tax_rate / 100.0) * current_net_amount

View File

@@ -7,7 +7,7 @@ import json
import frappe
from frappe import _
from frappe.modules.utils import get_module_app
from frappe.utils import flt, has_common
from frappe.utils import cint, flt, has_common
from frappe.utils.user import is_website_user
@@ -179,10 +179,13 @@ def get_list_for_transactions(
def rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length):
data = frappe.db.sql(
"""select distinct parent as name, supplier from `tab{doctype}`
where supplier = '{supplier}' and docstatus=1 order by creation desc limit {start}, {len}""".format(
doctype=parties_doctype, supplier=parties[0], start=limit_start, len=limit_page_length
),
f"""select distinct parent as name, supplier from `tab{parties_doctype}`
where supplier = %(supplier)s and docstatus=1 order by creation desc limit %(start)s, %(len)s""",
{
"supplier": parties[0],
"start": cint(limit_start),
"len": cint(limit_page_length),
},
as_dict=1,
)